Notes › Tech Notes › A 60-bit Timestamp Leaves 28 Positions on This Page: Shifts and Masks in the UUID Generator v1 Branch, Measured Millisecond by Millisecond

A 60-bit Timestamp Leaves 28 Positions on This Page: Shifts and Masks in the UUID Generator v1 Branch, Measured Millisecond by Millisecond

This page reads the timestamp into a single 32-bit window: shift counts wrap modulo 32, so time_mid and time_hi re-read the low bits, leaving 28 positions touched and only 24 that change; on the negative leg just 31 hex digits survive.

Which bits of the timestamp does the v1 branch on this page actually read

The timestamp segment of the v1 branch on this page looks like a 60-bit field, but once it is run millisecond by millisecond what is really read turns out to be a single 32-bit window. generateV1 sits on lines 126 to 162 of the template; line 131 computes ts = (now - the 1582 epoch) * 10000, and lines 134, 135 and 136 then take ts & 0xFFFFFFFF, (ts >>> 32) & 0xFFFF and (ts >>> 48) & 0x0FFF. Every statement here is about this page and this branch only, and none of it is extended to other UUID libraries.

Shift counts wrap modulo 32: 60 bits collapse into 28 positions, 24 that change

The shift count of >>> is taken modulo 32, so >>> 32 behaves as >>> 0 and >>> 48 behaves as >>> 16. Over 20000 consecutive milliseconds both timeMid === (timeLow & 0xFFFF) and timeHi === ((timeLow >>> 16) & 0x0FFF) held, 20000/20000 times: time_mid is nothing but a re-read of the low 16 bits of time_low, and the low 12 bits of time_hi_and_version are a re-read of bits 16 to 27 of that same window. Take the union of every bit the three fields touch and it is bits 0 through 27, 28 positions in all; nothing at or above bit 32 ever reaches the output.

Even fewer bits move than the 28 that are read. Scanning 2000000 millisecond steps gives a bitwise OR of 0xfffffff0 and a bitwise AND of 0, which pins bits 0 to 3 at zero all the time; intersect that with the 28 positions the fields actually read and only 24 bits really change. Line 138, timeHi | 0x1000, then nails the version nibble to a constant one on top of that.

the timestamp lands on one 32-bit window bits 0..31 = time_low; read bits 0..27, 28 in all bits 0..3 stuck at 0; moving bits = 24 OR = 0xfffffff0, AND = 0 (2000000 ms scan) timeMid = timeLow & 0xFFFF: 20000/20000 timeMid and timeHi take 4096 values each

Figure 1: all three reads re-read the same window

What that costs in practice is easy to state without any formula. A real version-one timestamp is supposed to count hundred-nanosecond ticks since the epoch that branch defines for itself, while here every bit above the low word is thrown away before the string is assembled, so two moments that differ only high in the word collapse onto the same segment.

The stuck-at-zero low nibble lands on character 8

The timestamp is always a multiple of 16: over 20000 milliseconds ts % 16 === 0 held 20000/20000 times, while ts % 32 splits evenly between 0 and 16 at 10000 each - being "a multiple of 32" is only true on half of the milliseconds, which is why a spot check is not a proof. Carried onto the string, the tail of the time_low segment, that is character 8, came out as a constant zero in all 20000 samples and in an aligned 100-line batch. The very last character of the whole string, the low nibble of the node segment, is the one that really spreads over 16 values, and it printed a zero only 3/100 times.

timeLow goes through ToInt32: a positive and a negative leg that flip by the millisecond

timeLow = ts & 0xFFFFFFFF is a signed Int32, so neighbouring milliseconds hand out positives and negatives alike. The full cycle is 2^32 / 10000 = 429496.7296 ms, that is 7.1583 minutes, and the negative half of it is 2^31 / 10000 = 214748.3648 ms, that is 3.5791 minutes. On a real, unstubbed clock the first flip needed only 109614 ms; a scan of 2400000 milliseconds puts negatives at 1242928 steps = 51.79% of them. The impression that a negative shows up once a year is only an artefact of sampling one moment per year.

timeLow goes through ToInt32: two legs full cycle 429496.7296 ms = 7.1583 minutes negative leg 214748.3648 ms = 3.5791 minutes deep -68b10840: 36 units, 31 hex digits left shallow -1b80: padStart puts 0 before the sign hyphens off reveals a length of 33 or 32 units

Figure 2: the period and the shape of the negative leg

The negative leg does not change the length of a line, only what is inside it. For the deep case -68b10840 the minus signs sit at indexes [0,8,13,18,23]. For the shallow case -1b80 the leading minus is pushed to index 3, because line 153 runs padStart(8,'0') and the padding zeros are inserted ahead of the sign rather than after it. Line 98, substr(20,12), consumes only the first 32 characters and silently drops the last nibble of the node segment, so 31 hex digits survive; a batch of 1000 negative lines distributes as {36:1000} over lengths and {5:1000} over minus signs per line.

The rounding boundary sits at 2^57, not 2^53

Right now ts already exceeds 2^53 by about 15.56 times and falls between 2^56 and 2^57, where the spacing between representable doubles, the ULP, is exactly 16. Since diff × 10000 is always a multiple of 16, because 10000 = 16 × 625, the product already lands on that grid, so a run of 1000 milliseconds today rounds 0 lines. Real rounding waits until ts crosses 2^57 and the ULP widens to 32: stubbed to 2039-06-20T23:40:07.585Z, over the next 1000 milliseconds 499 lines are rounded, and the run stubbed at 2045-01-01 gives 500. The 2^53 mark corresponds to the year 1611, a moment this code path can never arrive at.

Input truth table: the count box, the switches and the two masks

Input itemHow it was measuredResult
17 inputs typed into the count boxThree-stage fallback: convert to an integer first, then || 1, then clamp between 1 and 1000, an empty string and "abc" each produce 1 line and write 1 back into the box; 101, 1000 and 1000000 each produce 100 lines and write 100 back; "2.7" truncates to 2 lines
The three switches in 8 combinationsLength is decided by hyphens and braces alone; case never changes it32 / 34 (braces added) / 36 (hyphens added) / 38 (both added) code units, four values only
The two masks of the v4 branchLines 115, 116Character 15 is a constant 4, 100/100; character 20 falls in 8/9/a/b with distribution {8:26, 9:23, a:29, b:22}
nodeBytes[0] | 0x01Line 149 sets the low bit of the first byteIt lands on character 2 of the node segment, odd 1000/1000 times, while character 1 comes out odd only 517/1000 times

Batches and clearing the board

Stubbed into a single millisecond with count=100, the 100 v1 lines come out identical over their first 14 characters, distinct=1, and every difference comes from the 4 characters of clock_seq and the 12 characters of the node segment. On a real clock, 1000 calls to generateV1() span 14 ms, with distinct=13 for the time_low segment, distinct=966 for the clock_seq segment and 1000 for the whole string. Clearing the board leaves residue: once lastUuids is emptied, renderList() only writes the result card display to none and resets the statistics cell to zero, while the innerHTML of the result container still holds 1285 code units.

Against the listing page copy

Compared entry by entry with the intro text in tools.php there are 25 judgements: 11 agree, 4 conflict and 10 partially agree. The conflict that matters most is the sentence "8-4-4-4-12, 32 hexadecimal characters in all": an aligned batch does line up with 32, but on this page the negative leg leaves 31. The other one is that the keywords list spells out GUID while the template never contains that word anywhere. The claim that v1 is generated from the timestamp plus node information is a partial match, because the node segment here is re-rolled random bytes on every call and no network adapter is read at all. The version bit and the variant bit are recorded in the older UUID generator article, and its scenario and probability narrative is not repeated here; the random segment on this page belongs to the same interface family as random password generation, and the quality of that entropy source was not measured; for the live details see the UUID generator tool page.

FAQ

Q: Is the v1 timestamp segment on this page the standard 60 bits?

No. All three fields draw their values from one 32-bit window, so the bits read are bits 0 through 27, 28 positions, and once the stuck-at-zero low nibble is removed only 24 bits actually change from one millisecond to the next.

Q: Why does a minus sign sometimes appear inside the uuid, with one hex digit missing?

Because timeLow goes through ToInt32 and lands on the negative leg in close to half of all milliseconds, and substr(20,12) then cuts off the final nibble of the node segment, so 31 hex digits are left.

📎 Related articles

The color picker parse gate accepts only six-digit hex, making the three-digit branch unreachable on the page 2026-11-08 Editing the Salt Changes Nothing Until You Click: Three-Segment Concatenation and Multiline Input Measured on the MD5 Page 2026-11-08 The Count Reads 5 or 1: Three Switches and Zero-Width Matches Measured in Batch Find and Replace 2026-11-07 Clear One Box and 0 Cards Show: Silent Failure, Skipping Row Labels and 990 Unreduced Rows 2026-11-07 text-line-ops: delete right gives an empty string, cut right hands back the whole line 2026-11-07