Skip to content

Commit 27713cd

Browse files
[Uid] Add microsecond precision to UUIDv7 and optimize on x64
1 parent a3c1d1f commit 27713cd

File tree

3 files changed

+58
-31
lines changed

3 files changed

+58
-31
lines changed

src/Symfony/Component/Uid/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
CHANGELOG
22
=========
33

4+
7.4
5+
---
6+
7+
* Add microsecond precision to UUIDv7
8+
49
7.3
510
---
611

src/Symfony/Component/Uid/Tests/Command/InspectUuidCommandTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,7 @@ public function testV7()
239239
toBase32 01FWHE4YDGFK1SHH6W1G60EECF
240240
toHex 0x017f22e279b07cc398c4dc0c0c07398f
241241
----------------------- --------------------------------------
242-
Time 2022-02-22 19:22:22.000000 UTC
242+
Time 2022-02-22 19:22:22.000816 UTC
243243
----------------------- --------------------------------------
244244
245245

src/Symfony/Component/Uid/UuidV7.php

Lines changed: 52 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,11 @@
1414
use Symfony\Component\Uid\Exception\InvalidArgumentException;
1515

1616
/**
17-
* A v7 UUID is lexicographically sortable and contains a 48-bit timestamp and 74 extra unique bits.
17+
* A v7 UUID is lexicographically sortable and contains a 58-bit timestamp and 64 extra unique bits.
1818
*
19-
* Within the same millisecond, monotonicity is ensured by incrementing the random part by a random increment.
19+
* Within the same millisecond, the unique bits are incremented by a 24-bit random number.
20+
* This method provides microsecond precision for the timestamp, and minimizes both the
21+
* risk of collisions and the consumption of the OS' entropy pool.
2022
*
2123
* @author Nicolas Grekas <p@tchwork.com>
2224
*/
@@ -25,6 +27,7 @@ class UuidV7 extends Uuid implements TimeBasedUidInterface
2527
protected const TYPE = 7;
2628

2729
private static string $time = '';
30+
private static int $subMs = 0;
2831
private static array $rand = [];
2932
private static string $seed;
3033
private static array $seedParts;
@@ -47,23 +50,27 @@ public function getDateTime(): \DateTimeImmutable
4750
if (4 > \strlen($time)) {
4851
$time = '000'.$time;
4952
}
53+
$time .= substr(1000 + (hexdec(substr($this->uid, 14, 4)) >> 2 & 0x3FF), -3);
5054

51-
return \DateTimeImmutable::createFromFormat('U.v', substr_replace($time, '.', -3, 0));
55+
return \DateTimeImmutable::createFromFormat('U.u', substr_replace($time, '.', -6, 0));
5256
}
5357

5458
public static function generate(?\DateTimeInterface $time = null): string
5559
{
5660
if (null === $mtime = $time) {
5761
$time = microtime(false);
62+
$subMs = (int) substr($time, 5, 3);
5863
$time = substr($time, 11).substr($time, 2, 3);
59-
} elseif (0 > $time = $time->format('Uv')) {
64+
} elseif (0 > $time = $time->format('Uu')) {
6065
throw new InvalidArgumentException('The timestamp must be positive.');
66+
} else {
67+
$subMs = (int) substr($time, -3);
68+
$time = substr($time, 0, -3);
6169
}
6270

6371
if ($time > self::$time || (null !== $mtime && $time !== self::$time)) {
6472
randomize:
65-
self::$rand = unpack('n*', isset(self::$seed) ? random_bytes(10) : self::$seed = random_bytes(16));
66-
self::$rand[1] &= 0x03FF;
73+
self::$rand = unpack(\PHP_INT_SIZE >= 8 ? 'L*' : 'n*', isset(self::$seed) ? random_bytes(8) : self::$seed = random_bytes(16));
6774
self::$time = $time;
6875
} else {
6976
// Within the same ms, we increment the rand part by a random 24-bit number.
@@ -73,8 +80,8 @@ public static function generate(?\DateTimeInterface $time = null): string
7380
// them into 16 x 32-bit numbers; we take the first byte of each of these
7481
// numbers to get 5 extra 24-bit numbers. Then, we consume those numbers
7582
// one-by-one and run this logic every 21 iterations.
76-
// self::$rand holds the random part of the UUID, split into 5 x 16-bit
77-
// numbers for x86 portability. We increment this random part by the next
83+
// self::$rand holds the random part of the UUID, split into 2 x 32-bit numbers
84+
// or 4 x 16-bit for x86 portability. We increment this random part by the next
7885
// 24-bit number in the self::$seedParts list and decrement self::$seedIndex.
7986

8087
if (!self::$seedIndex) {
@@ -88,40 +95,55 @@ public static function generate(?\DateTimeInterface $time = null): string
8895
self::$seedIndex = 21;
8996
}
9097

91-
self::$rand[5] = 0xFFFF & $carry = self::$rand[5] + 1 + (self::$seedParts[self::$seedIndex--] & 0xFFFFFF);
92-
self::$rand[4] = 0xFFFF & $carry = self::$rand[4] + ($carry >> 16);
93-
self::$rand[3] = 0xFFFF & $carry = self::$rand[3] + ($carry >> 16);
94-
self::$rand[2] = 0xFFFF & $carry = self::$rand[2] + ($carry >> 16);
95-
self::$rand[1] += $carry >> 16;
96-
97-
if (0xFC00 & self::$rand[1]) {
98-
if (\PHP_INT_SIZE >= 8 || 10 > \strlen($time = self::$time)) {
99-
$time = (string) (1 + $time);
100-
} elseif ('999999999' === $mtime = substr($time, -9)) {
101-
$time = (1 + substr($time, 0, -9)).'000000000';
102-
} else {
103-
$time = substr_replace($time, str_pad(++$mtime, 9, '0', \STR_PAD_LEFT), -9);
104-
}
98+
if (\PHP_INT_SIZE >= 8) {
99+
self::$rand[2] = 0xFFFFFFFF & $carry = self::$rand[2] + 1 + (self::$seedParts[self::$seedIndex--] & 0xFFFFFF);
100+
self::$rand[1] = 0xFFFFFFFF & $carry = self::$rand[1] + ($carry >> 32);
101+
$carry >>= 32;
102+
} else {
103+
self::$rand[4] = 0xFFFF & $carry = self::$rand[4] + 1 + (self::$seedParts[self::$seedIndex--] & 0xFFFFFF);
104+
self::$rand[3] = 0xFFFF & $carry = self::$rand[3] + ($carry >> 16);
105+
self::$rand[2] = 0xFFFF & $carry = self::$rand[2] + ($carry >> 16);
106+
self::$rand[1] = 0xFFFF & $carry = self::$rand[1] + ($carry >> 16);
107+
$carry >>= 16;
108+
}
109+
110+
if ($carry && $subMs <= self::$subMs) {
111+
usleep(1);
105112

106-
goto randomize;
113+
if (1024 <= ++$subMs) {
114+
if (\PHP_INT_SIZE >= 8 || 10 > \strlen($time = self::$time)) {
115+
$time = (string) (1 + $time);
116+
} elseif ('999999999' === $mtime = substr($time, -9)) {
117+
$time = (1 + substr($time, 0, -9)).'000000000';
118+
} else {
119+
$time = substr_replace($time, str_pad(++$mtime, 9, '0', \STR_PAD_LEFT), -9);
120+
}
121+
122+
goto randomize;
123+
}
107124
}
108125

109126
$time = self::$time;
110127
}
128+
self::$subMs = $subMs;
111129

112130
if (\PHP_INT_SIZE >= 8) {
113-
$time = dechex($time);
114-
} else {
115-
$time = bin2hex(BinaryUtil::fromBase($time, BinaryUtil::BASE10));
131+
return substr_replace(\sprintf('%012x-%04x-%04x-%04x%08x',
132+
$time,
133+
0x7000 | ($subMs << 2) | (self::$rand[1] >> 30),
134+
0x8000 | (self::$rand[1] >> 16 & 0x3FFF),
135+
self::$rand[1] & 0xFFFF,
136+
self::$rand[2],
137+
), '-', 8, 0);
116138
}
117139

118140
return substr_replace(\sprintf('%012s-%04x-%04x-%04x%04x%04x',
119-
$time,
120-
0x7000 | (self::$rand[1] << 2) | (self::$rand[2] >> 14),
121-
0x8000 | (self::$rand[2] & 0x3FFF),
141+
bin2hex(BinaryUtil::fromBase($time, BinaryUtil::BASE10)),
142+
0x7000 | ($subMs << 2) | (self::$rand[1] >> 14),
143+
0x8000 | (self::$rand[1] & 0x3FFF),
144+
self::$rand[2],
122145
self::$rand[3],
123146
self::$rand[4],
124-
self::$rand[5],
125147
), '-', 8, 0);
126148
}
127149
}

0 commit comments

Comments
 (0)