Skip to content

Commit

Permalink
optimization & benchmarking
Browse files Browse the repository at this point in the history
  • Loading branch information
abmmhasan committed Apr 30, 2024
1 parent 69b6faf commit d0ec53a
Show file tree
Hide file tree
Showing 2 changed files with 84 additions and 56 deletions.
61 changes: 40 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ UUID (RFC 4122 + Unofficial/Draft), ULID, Snowflake ID, Sonyflake ID, TBSL (libr
## Table of contents

<!--ts-->

* [Prerequisites](#prerequisites)
* [Installation](#installation)
* [Usage](#usage)
Expand All @@ -32,6 +33,7 @@ UUID (RFC 4122 + Unofficial/Draft), ULID, Snowflake ID, Sonyflake ID, TBSL (libr
* [TBSL ID](#tbsl-time-based-keys-with-lexicographic-sorting)
* [Support](#support)
* [References](#references)

<!--te-->

## Prerequisites
Expand All @@ -48,7 +50,8 @@ composer require infocyph/uid

### UUID (Universal Unique Identifier)

The node specific UUID's `$node` parameter (1, 6, 7, 8) is optional. If not provided, it will be generated randomly. But,
The node specific UUID's `$node` parameter (1, 6, 7, 8) is optional. If not provided, it will be generated randomly.
But,
if you wanna track the source of the UUIDs, you should use it (pre-define the node per server & pass it accordingly).

#### UUID v1: Time-based UUID.
Expand All @@ -60,11 +63,8 @@ if you wanna track the source of the UUIDs, you should use it (pre-define the no
// alternatively can also use
\Infocyph\UID\uuid1();

// Get generated node, for further use
$node = \Infocyph\UID\UUID::getNode(1);

// Pass your pre-generated node (for node specific UUID)
\Infocyph\UID\UUID::v1($node);
\Infocyph\UID\UUID::v1($node); // check additional section for how to generate one
```

#### UUID v3: Namespace based UUID.
Expand Down Expand Up @@ -98,6 +98,8 @@ $node = \Infocyph\UID\UUID::getNode(1);

#### UUID v5: Namespace based UUID.

Better replacement for v3 due to better hashing algorithm (SHA1 instead of MD5).

```php
// Get v5 UUID for 'TestString'
\Infocyph\UID\UUID::v5('TestString');
Expand All @@ -117,18 +119,17 @@ $node = \Infocyph\UID\UUID::getNode(1);

#### UUID v6 (draft-based/unofficial): Time-based UUID.

Better replacement for v1. Provides more randomness & uniqueness.

```php
// Get v6 UUID (Time based)
\Infocyph\UID\UUID::v6();

// alternatively can also use
\Infocyph\UID\uuid6();

// Get generated node, for further use
$node = \Infocyph\UID\UUID::getNode(6);

// Pass your pre-generated node (for node specific UUID)
\Infocyph\UID\UUID::v6($node);
\Infocyph\UID\UUID::v6($node); // check additional section for how to generate one
```

#### UUID v7 (draft-based/unofficial): Time-based UUID.
Expand All @@ -140,11 +141,8 @@ $node = \Infocyph\UID\UUID::getNode(6);
// alternatively can also use
\Infocyph\UID\uuid7();

// Get generated node, for further use
$node = \Infocyph\UID\UUID::getNode(7);

// Pass your pre-generated node (for node specific UUID)
\Infocyph\UID\UUID::v7($node);
\Infocyph\UID\UUID::v7($node); // check additional section for how to generate one
```

#### UUID v8 (draft-based/unofficial): Time-based UUID. Lexicographically sortable.
Expand All @@ -156,16 +154,16 @@ $node = \Infocyph\UID\UUID::getNode(7);
// alternatively can also use
\Infocyph\UID\uuid8();

// Get generated node, for further use
$node = \Infocyph\UID\UUID::getNode(8);

// Pass your pre-generated node (for node specific UUID)
\Infocyph\UID\UUID::v8($node);
\Infocyph\UID\UUID::v8($node); // check additional section for how to generate one
```

#### Additional

```php
// Generate node for further use (with version: 1, 6, 7, 8)
\Infocyph\UID\UUID::getNode();

// Parse any UUID string
\Infocyph\UID\UUID::parse($uuid); // returns ['isValid', 'version', 'time', 'node']
```
Expand All @@ -187,7 +185,7 @@ $node = \Infocyph\UID\UUID::getNode(8);

```php
// Get Snowflake ID
// optionally set worker_id & datacenter_id
// optionally you can set worker_id & datacenter_id, for server/module detection
\Infocyph\UID\Snowflake::generate();
// alternatively
\Infocyph\UID\snowflake();
Expand All @@ -198,15 +196,15 @@ $node = \Infocyph\UID\UUID::getNode(8);

// By default, the start time is set to `2020-01-01 00:00:00`, which is changeable
// but if changed, this should always stay same as long as your project lives
// & must call this before any Sonyflake call (generate/parse)
// & must call this before any Snowflake call (generate/parse)
\Infocyph\UID\Snowflake::setStartTimeStamp('2000-01-01 00:00:00');
```

### Sonyflake ID

```php
// Get Sonyflake ID
// optionally set machine_id
// optionally set machine_id, for server detection
\Infocyph\UID\Sonyflake::generate();
// alternatively
\Infocyph\UID\sonyflake();
Expand All @@ -222,11 +220,12 @@ $node = \Infocyph\UID\UUID::getNode(8);
```

### TBSL: Time-Based Keys with Lexicographic Sorting

Library exclusive.

```php
// Get TBSL ID
// optionally set machine_id
// optionally set machine_id, for server detection
\Infocyph\UID\TBSL::generate();
// alternatively
\Infocyph\UID\tbsl();
Expand All @@ -236,6 +235,26 @@ Library exclusive.
\Infocyph\UID\TBSL::parse($tbsl);
```

## Benchmark

| Type | Total Duration (1M Generation, Single thread) |
|:---------------------------|:---------------------------------------------:|
| UUID v1 (random node) | 1.23968s |
| UUID v1 (fixed node) | 0.94521s |
| UUID v3 (custom namespace) | 0.76439s |
| UUID v4 | 0.70501s |
| UUID v5 (custom namespace) | 0.89831s |
| UUID v6 (random node) | 1.39867s |
| UUID v6 (fixed node) | 1.42344s |
| UUID v7 (random node) | 1.40466s |
| UUID v7 (fixed node) | 1.49268s |
| UUID v8 (random node) | 1.80438s |
| UUID v8 (fixed node) | 1.78257s |
| ULID | 1.79775s |
| TBSL | 0.80612s |

_Note: Snowflake & Sonyflake not included, due to their way of work_

## Support

Having trouble? Create an issue!
Expand Down
79 changes: 44 additions & 35 deletions src/UUID.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,10 @@ class UUID
private static int $secondIntervals = 10_000_000;
private static int $secondIntervals78 = 10_000;
private static int $timeOffset = 0x01b21dd213814000;
private static array $nodeLength = [
1 => 6,
6 => 8,
7 => 10,
8 => 7
private static array $randomLength = [
6 => 2,
7 => 4,
8 => 1
];

/**
Expand All @@ -59,7 +58,7 @@ public static function v1(string $node = null): string
substr($time, -12, 4),
substr($time, -15, 3),
$clockSeq | 0x8000,
$node ?? self::getNode(1)
$node ?? self::getNode()
);
}

Expand Down Expand Up @@ -128,7 +127,7 @@ public static function v6(string $node = null): string
'6',
-3,
0
) . ($node ?? self::getNode(6));
) . self::prepareNode(6, $node);
return self::output(6, $string);
}

Expand All @@ -148,7 +147,7 @@ public static function v7(string $node = null): string
self::$unixTsMs = $unixTsMs;

$string = substr(str_pad(dechex($unixTsMs), 12, '0', STR_PAD_LEFT), -12)
. ($node ?? self::getNode(7));
. self::prepareNode(7, $node);
return self::output(7, $string);
}

Expand All @@ -168,20 +167,35 @@ public static function v8(string $node = null): string
$string = substr(str_pad(dechex($unixTsMs), 12, '0', STR_PAD_LEFT), -12) .
'8' . str_pad(dechex($subSecA), 3, '0', STR_PAD_LEFT) .
bin2hex(chr(ord(random_bytes(1)) & 0x0f | ($subSec & 0x03) << 4)) .
($node ?? self::getNode(8));
self::prepareNode(8, $node);
return self::output(8, $string);
}

/**
* Generate unique hexadecimal node.
* Generates a random node string based on the given version and node.
*
* @param int $version The version of the UUID.
* @return string The generated hexadecimal node.
* @param int $version The version of the node.
* @param string|null $node The node identifier. Defaults to null.
* @return string The generated node string.
* @throws Exception
*/
private static function prepareNode(int $version, string $node = null): string
{
if (!$node) {
return bin2hex(random_bytes(self::$randomLength[$version] + 6));
}
return bin2hex(random_bytes(self::$randomLength[$version])) . $node;
}

/**
* Generate unique node.
*
* @return string The generated node.
* @throws Exception
*/
public static function getNode(int $version): string
public static function getNode(): string
{
return bin2hex(random_bytes(self::$nodeLength[$version]));
return bin2hex(random_bytes(6));
}

/**
Expand All @@ -204,14 +218,10 @@ public static function parse(string $uuid): array
return $data;
}

$data['version'] = (int)$uuid[14];

$timeNodeApplicable = in_array($data['version'], [1, 6, 7, 8]);
$data['time'] = $timeNodeApplicable ? self::getTime($uuid) : null;
$data['node'] = $timeNodeApplicable ? substr(
str_replace('-', '', $uuid),
-(self::$nodeLength[$data['version']] * 2)
) : null;
$uuidData = explode('-', $uuid);
$data['version'] = (int)$uuidData[2][0];
$data['time'] = in_array($data['version'], [1, 6, 7, 8]) ? self::getTime($uuidData, $data['version']) : null;
$data['node'] = $uuidData[4];

return $data;
}
Expand All @@ -230,14 +240,13 @@ private static function isValid(string $uuid): bool
/**
* Retrieves the time from the UUID.
*
* @param string $uuid The UUID string to extract time from.
* @param array $uuid The UUID array to extract time from.
* @param int $version The version of the UUID.
* @return DateTimeInterface The DateTimeImmutable object representing the extracted time.
* @throws UUIDException|Exception
*/
private static function getTime(string $uuid): DateTimeInterface
private static function getTime(array $uuid, int $version): DateTimeInterface
{
$uuid = explode('-', $uuid);
$version = (int)$uuid[2][0];
$timestamp = match ($version) {
1 => substr($uuid[2], -3) . $uuid[1] . $uuid[0],
6, 8 => $uuid[0] . $uuid[1] . substr($uuid[2], -3),
Expand All @@ -256,7 +265,7 @@ private static function getTime(string $uuid): DateTimeInterface
(hexdec(substr('0' . $timestamp, 13)) << 2) +
(hexdec($uuid[3][0]) & 0x03)
) * self::$secondIntervals78 >> 14);
$time = str_split(strval($unixTs * self::$secondIntervals78 + $subSec), 10);
$time = str_split((string)($unixTs * self::$secondIntervals78 + $subSec), 10);
$time[1] = substr($time[1], 0, 6);
break;
default:
Expand Down Expand Up @@ -289,8 +298,8 @@ private static function getUnixTimeSubSec(int $version = 1): array
}
if (
self::$unixTs[$version] > $unixTs ||
self::$unixTs[$version] === $unixTs &&
self::$subSec[$version] >= $subSec
(self::$unixTs[$version] === $unixTs &&
self::$subSec[$version] >= $subSec)
) {
$unixTs = self::$unixTs[$version];
$subSec = self::$subSec[$version];
Expand All @@ -310,12 +319,12 @@ private static function getUnixTimeSubSec(int $version = 1): array
* Generates a formatted string based on the given version and string.
*
* @param int $version The version number.
* @param string $string The input string.
* @param string $id The string to be formatted.
* @return string The formatted string.
*/
private static function output(int $version, string $string): string
private static function output(int $version, string $id): string
{
$string = str_split($string, 4);
$string = str_split($id, 4);
return sprintf(
"%08s-%04s-$version%03s-%04x-%012s",
$string[0] . $string[1],
Expand All @@ -330,9 +339,9 @@ private static function output(int $version, string $string): string
* Resolves the given namespace.
*
* @param string $namespace The namespace to be resolved.
* @return string|array|bool The resolved namespace or false if it cannot be resolved.
* @return string The resolved namespace or false if it cannot be resolved.
*/
private static function nsResolve(string $namespace): string|array|bool
private static function nsResolve(string $namespace): string
{
if (self::isValid($namespace)) {
return str_replace('-', '', $namespace);
Expand All @@ -341,6 +350,6 @@ private static function nsResolve(string $namespace): string|array|bool
if (isset(self::$nsList[$namespace])) {
return "6ba7b81" . self::$nsList[$namespace] . "9dad11d180b400c04fd430c8";
}
return false;
return '';
}
}

0 comments on commit d0ec53a

Please sign in to comment.