@cdlab996/genid
Advanced tools
+377
| # @cdlab996/genid | ||
| [](https://www.npmjs.com/package/@cdlab996/genid) | ||
| [](./LICENSE) | ||
| 基于 Snowflake 算法的高性能分布式唯一 ID 生成器,支持漂移算法和时钟回拨处理。 | ||
| [English](./README.md) | ||
| ## 特性 | ||
| - **漂移算法** - 高并发场景下突破每毫秒序列号上限,性能更优 | ||
| - **时钟回拨处理** - 使用保留序列号优雅降级,不阻塞 ID 生成 | ||
| - **灵活配置** - 支持自定义时间戳、节点 ID、序列号的位长度分配 | ||
| - **ID 验证** - 支持严格/宽松模式校验,支持 `afterTime` 时间下限约束 | ||
| - **运行监控** - 内置统计、解析和二进制格式化调试工具 | ||
| ## 安装 | ||
| ```bash | ||
| # npm | ||
| npm install @cdlab996/genid | ||
| # pnpm | ||
| pnpm add @cdlab996/genid | ||
| ``` | ||
| ## 快速开始 | ||
| ```typescript | ||
| import { GenidOptimized } from '@cdlab996/genid' | ||
| // 创建实例(每个 Worker/进程使用不同的 workerId) | ||
| const genid = new GenidOptimized({ workerId: 1 }) | ||
| // 生成 ID | ||
| const id = genid.nextId() | ||
| // 批量生成 | ||
| const ids = genid.nextBatch(1000) | ||
| // 解析 ID | ||
| const info = genid.parse(id) | ||
| // => { timestamp: Date, timestampMs: 1609459200000, workerId: 1, sequence: 42 } | ||
| // 验证 ID | ||
| genid.isValid(id) // true | ||
| ``` | ||
| ## API | ||
| ### `new GenidOptimized(options)` | ||
| | 参数 | 类型 | 必填 | 默认值 | 说明 | | ||
| | ------------------- | ------------- | :---: | ------------------ | --------------------------------------------- | | ||
| | `workerId` | `number` | Yes | - | 工作节点 ID(0 ~ 2^workerIdBitLength-1) | | ||
| | `method` | `GenidMethod` | | `DRIFT` | 算法:`DRIFT`(漂移)或 `TRADITIONAL`(传统) | | ||
| | `baseTime` | `number` | | `1577836800000` | 起始时间戳,毫秒(默认 2020-01-01) | | ||
| | `workerIdBitLength` | `number` | | `6` | 节点 ID 位数(1-15) | | ||
| | `seqBitLength` | `number` | | `6` | 序列号位数(3-21) | | ||
| | `maxSeqNumber` | `number` | | `2^seqBitLength-1` | 最大序列号 | | ||
| | `minSeqNumber` | `number` | | `5` | 最小序列号(0-4 保留用于时钟回拨) | | ||
| | `topOverCostCount` | `number` | | `2000` | 最大漂移次数 | | ||
| ### 生成 ID | ||
| ```typescript | ||
| genid.nextId() // 返回 number | bigint(自动选择) | ||
| genid.nextNumber() // 返回 number(超出安全整数范围抛错) | ||
| genid.nextBigId() // 返回 bigint | ||
| genid.nextBatch(100) // 批量生成 100 个 ID | ||
| genid.nextBatch(100, true) // 批量生成 100 个 BigInt ID | ||
| ``` | ||
| ### 解析与验证 | ||
| ```typescript | ||
| // 解析 ID 的组成部分 | ||
| genid.parse(id) | ||
| // => { timestamp: Date, timestampMs: number, workerId: number, sequence: number } | ||
| // 宽松验证:检查 ID 结构是否有效 | ||
| genid.isValid(id) // true | ||
| genid.isValid('invalid') // false | ||
| // 严格验证:要求 workerId 匹配当前实例 | ||
| genid.isValid(id, true) // true(本实例生成的 ID) | ||
| genid.isValid(otherId, true) // false(其他实例生成的 ID) | ||
| // 时间下限验证:拒绝早于指定时间生成的 ID | ||
| const startupTime = Date.now() | ||
| genid.isValid(id, { afterTime: startupTime }) // true | ||
| genid.isValid(id, { strictWorkerId: true, afterTime: startupTime }) // 组合使用 | ||
| ``` | ||
| ### 统计与配置 | ||
| ```typescript | ||
| // 获取运行统计 | ||
| genid.getStats() | ||
| // => { | ||
| // totalGenerated: 1000, | ||
| // overCostCount: 10, | ||
| // turnBackCount: 2, | ||
| // uptimeMs: 60000, | ||
| // avgPerSecond: 16, | ||
| // currentState: 'NORMAL' | 'OVER_COST' | ||
| // } | ||
| // 获取当前配置 | ||
| genid.getConfig() | ||
| // => { | ||
| // method: 'DRIFT', | ||
| // workerId: 1, | ||
| // workerIdRange: '0-63', | ||
| // sequenceRange: '5-63', | ||
| // maxSequence: 63, | ||
| // idsPerMillisecond: 59, | ||
| // baseTime: Date, | ||
| // timestampBits: 52, | ||
| // workerIdBits: 6, | ||
| // sequenceBits: 6 | ||
| // } | ||
| // 重置统计 | ||
| genid.resetStats() | ||
| ``` | ||
| ### 调试 | ||
| ```typescript | ||
| genid.formatBinary(id) | ||
| // ID: 123456789012345 | ||
| // Binary (64-bit): | ||
| // 0000000000011010... - Timestamp (52 bits) = 2025-10-17T... | ||
| // 000001 - Worker ID (6 bits) = 1 | ||
| // 101010 - Sequence (6 bits) = 42 | ||
| ``` | ||
| ## 使用示例 | ||
| ### 自定义位分配 | ||
| ```typescript | ||
| import { GenidOptimized, GenidMethod } from '@cdlab996/genid' | ||
| const genid = new GenidOptimized({ | ||
| workerId: 1, | ||
| method: GenidMethod.TRADITIONAL, | ||
| baseTime: new Date('2024-01-01').valueOf(), | ||
| workerIdBitLength: 10, // 支持 1024 个节点 | ||
| seqBitLength: 12, // 每毫秒 4096 个 ID | ||
| topOverCostCount: 5000, | ||
| }) | ||
| ``` | ||
| ### 验证外部 ID | ||
| ```typescript | ||
| // 验证从数据库或 API 获取的 ID | ||
| const externalId = '123456789012345' | ||
| if (genid.isValid(externalId)) { | ||
| const info = genid.parse(externalId) | ||
| console.log('生成时间:', info.timestamp) | ||
| console.log('来自节点:', info.workerId) | ||
| } else { | ||
| console.error('无效 ID') | ||
| } | ||
| ``` | ||
| ### 性能监控 | ||
| ```typescript | ||
| setInterval(() => { | ||
| const stats = genid.getStats() | ||
| console.log(`速率: ${stats.avgPerSecond} ID/s | 漂移: ${stats.overCostCount} | 回拨: ${stats.turnBackCount}`) | ||
| }, 10000) | ||
| ``` | ||
| ## 算法模式 | ||
| | 模式 | 说明 | 适用场景 | | ||
| | ----------------- | ---------------------------------------- | -------------------- | | ||
| | **DRIFT**(默认) | 序列号耗尽时借用未来时间戳,避免等待 | 高频 ID 生成、高并发 | | ||
| | **TRADITIONAL** | 严格按时间戳递增,序列号耗尽等待下一毫秒 | 对时间顺序严格要求 | | ||
| ## 架构 | ||
| ### ID 结构(64-bit) | ||
| ``` | ||
| |------------ 时间戳 ------------|-- 工作节点 ID --|-- 序列号 --| | ||
| 42-52 bits 1-15 bits 3-21 bits | ||
| ``` | ||
| 默认配置:时间戳 52 bits(约 139 年)| 节点 ID 6 bits(64 个节点)| 序列号 6 bits(每毫秒 59 个 ID) | ||
| 序列号 `0-4` 保留用于时钟回拨,正常使用从 `5` 开始。 | ||
| ### 核心流程 | ||
| ```mermaid | ||
| graph TB | ||
| A[开始生成 ID] --> B{是否处于漂移状态?} | ||
| B -->|否| C[正常路径] | ||
| B -->|是| D[漂移路径] | ||
| C --> E{检测时钟} | ||
| E -->|时钟回拨| F[使用保留序列号 0-4] | ||
| E -->|时间前进| G[重置序列号] | ||
| E -->|同一毫秒| H{序列号是否溢出?} | ||
| H -->|否| I[序列号+1 正常生成] | ||
| H -->|是| J[进入漂移状态 时间戳+1] | ||
| D --> K{检测时间} | ||
| K -->|时间追上| L[退出漂移 恢复正常] | ||
| K -->|超过最大漂移| M[等待下一毫秒 退出漂移] | ||
| K -->|继续漂移| N{序列号是否溢出?} | ||
| N -->|否| O[使用当前序列号] | ||
| N -->|是| P[时间戳+1 重置序列号] | ||
| F --> Q[计算 ID] | ||
| G --> Q | ||
| I --> Q | ||
| J --> Q | ||
| L --> Q | ||
| M --> Q | ||
| O --> Q | ||
| P --> Q | ||
| Q --> R[更新统计] | ||
| R --> S[返回 ID] | ||
| ``` | ||
| ## 性能 | ||
| 吞吐量由每毫秒可用序列号槽位数决定,增大 `seqBitLength` 可线性提升: | ||
| | seqBitLength | 每毫秒槽位 | 吞吐量 | | ||
| | :-----------: | ---------: | ------------------: | | ||
| | 3 | 3 | ~3,000 IDs/sec | | ||
| | 4 | 11 | ~11,000 IDs/sec | | ||
| | **6**(默认) | **59** | **~58,000 IDs/sec** | | ||
| | 8 | 251 | ~247,000 IDs/sec | | ||
| | 10 | 1,019 | ~1,000,000 IDs/sec | | ||
| | 14 | 16,379 | ~4,500,000 IDs/sec | | ||
| > 基于 Node.js v22 (x64) 测量,实际结果因环境而异。 | ||
| > 运行 `pnpm run benchmark` 可探测当前机器的实际能力。 | ||
| | 指标 | 数值(默认配置) | | ||
| | -------------------- | ---------------: | | ||
| | 最大节点数 | 64 | | ||
| | 时间戳可用时长 | ~139 年 | | ||
| | P99 延迟(单次调用) | < 1µs | | ||
| ## Benchmark | ||
| 内置探测脚本,自动测量当前环境的实际性能上限: | ||
| ```bash | ||
| pnpm run benchmark | ||
| ``` | ||
| 输出内容: | ||
| 1. **单次调用吞吐量** — `nextId()` 峰值 IDs/sec | ||
| 2. **批量吞吐量** — 不同批次大小的 `nextBatch()` 性能 | ||
| 3. **延迟分位数** — P50 / P95 / P99 / P99.9 / Max | ||
| 4. **算法对比** — DRIFT vs TRADITIONAL 并排对比 | ||
| 5. **不同 seqBitLength 吞吐量** — 位分配对吞吐量的影响 | ||
| 6. **内存占用** — 生成 100 万 ID 后的堆内存变化 | ||
| 7. **推荐阈值** — 基于峰值 60% 给出的安全测试断言值 | ||
| <details> | ||
| <summary>输出示例</summary> | ||
| ```bash | ||
| station :: /app/projects/genid ‹main*› » pnpm run benchmark | ||
| > @cdlab996/genid@1.4.0 benchmark /app/projects/genid | ||
| > npx tsx scripts/benchmark.ts | ||
| ============================================================ | ||
| GenidOptimized — Environment Capability Probe | ||
| Node v22.22.0 | linux x64 | ||
| Date: 2026-04-01T08:41:07.669Z | ||
| ============================================================ | ||
| ──────────────────────────────────────────────────────────── | ||
| 1. Single-call throughput (nextId) | ||
| ──────────────────────────────────────────────────────────── | ||
| Duration: 3001ms | ||
| Generated: 226,073 | ||
| Throughput: 75,332 IDs/sec | ||
| ──────────────────────────────────────────────────────────── | ||
| 2. Batch throughput (nextBatch) | ||
| ──────────────────────────────────────────────────────────── | ||
| batch= 100 × 5000 => 61,665 IDs/sec | ||
| batch= 1,000 × 500 => 61,577 IDs/sec | ||
| batch= 10,000 × 50 => 61,716 IDs/sec | ||
| batch=100,000 × 5 => 61,644 IDs/sec | ||
| ──────────────────────────────────────────────────────────── | ||
| 3. Single-call latency percentiles | ||
| ──────────────────────────────────────────────────────────── | ||
| Samples: 100,000 | ||
| Avg: 0µs | ||
| P50: 0µs | ||
| P95: 1µs | ||
| P99: 1µs | ||
| P99.9: 1µs | ||
| Max: 364µs | ||
| ──────────────────────────────────────────────────────────── | ||
| 4. Algorithm comparison (DRIFT vs TRADITIONAL) | ||
| ──────────────────────────────────────────────────────────── | ||
| DRIFT 58,296 IDs/sec drift=1 | ||
| TRADITIONAL 59,000 IDs/sec drift=0 | ||
| ──────────────────────────────────────────────────────────── | ||
| 5. Throughput by sequence bit length | ||
| ──────────────────────────────────────────────────────────── | ||
| seqBits= 3 maxSeq= 7 => 2,986 IDs/sec drift=1 | ||
| seqBits= 4 maxSeq= 15 => 10,884 IDs/sec drift=1 | ||
| seqBits= 6 maxSeq= 63 => 58,240 IDs/sec drift=1 | ||
| seqBits= 8 maxSeq= 255 => 247,804 IDs/sec drift=1 | ||
| seqBits=10 maxSeq= 1023 => 1,008,930 IDs/sec drift=1 | ||
| seqBits=14 maxSeq=16383 => 4,130,701 IDs/sec drift=0 | ||
| ──────────────────────────────────────────────────────────── | ||
| 6. Memory footprint | ||
| ──────────────────────────────────────────────────────────── | ||
| Generated: 1,000,000 IDs (not stored) | ||
| Heap delta: -6.27 MB | ||
| Note: Run with --expose-gc for accurate GC-forced measurement | ||
| ──────────────────────────────────────────────────────────── | ||
| Summary — Recommended test thresholds | ||
| ──────────────────────────────────────────────────────────── | ||
| Peak single-call: 75,332 IDs/sec | ||
| Peak batch: 61,716 IDs/sec | ||
| Suggested min threshold: 45,199 IDs/sec (60% of peak) | ||
| Suggested batch threshold: 37,029 IDs/sec (60% of peak) | ||
| Suggested P99 cap: 2µs (3× measured P99) | ||
| ``` | ||
| </details> | ||
| ## 开发 | ||
| ```bash | ||
| pnpm install # 安装依赖 | ||
| pnpm run build # 构建(ESM + CJS) | ||
| pnpm run dev # 监听模式 | ||
| pnpm run test # 运行测试 | ||
| pnpm run benchmark # 运行环境性能探测 | ||
| pnpm run typecheck # 类型检查 | ||
| pnpm run lint # 代码检查(Biome) | ||
| pnpm run format # 代码格式化(Biome) | ||
| ``` | ||
| ## 注意事项 | ||
| - 每个 Worker/进程必须使用**不同的 workerId** | ||
| - 实例**非线程安全**,不要跨线程共享 | ||
| - `workerIdBitLength + seqBitLength` 不能超过 22 | ||
| - 序列号 0-4 保留用于时钟回拨处理 | ||
| - 超出 JavaScript 安全整数范围(2^53-1)时,使用 `nextBigId()` 或 `nextId()`(自动返回 BigInt) | ||
| ## License | ||
| [MIT](./LICENSE) License © 2025-PRESENT [wudi](https://github.com/WuChenDi) |
+20
-13
@@ -18,5 +18,5 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" }); | ||
| method: options.method === GenidMethod.TRADITIONAL ? GenidMethod.TRADITIONAL : GenidMethod.DRIFT, | ||
| baseTime: options.baseTime && options.baseTime > 0 ? options.baseTime : (/* @__PURE__ */ new Date("2020-01-01")).valueOf(), | ||
| workerIdBitLength: options.workerIdBitLength && options.workerIdBitLength > 0 ? options.workerIdBitLength : 6, | ||
| seqBitLength: options.seqBitLength && options.seqBitLength > 0 ? options.seqBitLength : 6, | ||
| baseTime: options.baseTime != null && options.baseTime > 0 ? options.baseTime : (/* @__PURE__ */ new Date("2020-01-01")).valueOf(), | ||
| workerIdBitLength: options.workerIdBitLength != null && options.workerIdBitLength > 0 ? options.workerIdBitLength : 6, | ||
| seqBitLength: options.seqBitLength != null && options.seqBitLength > 0 ? options.seqBitLength : 6, | ||
| maxSeqNumber: 0, | ||
@@ -26,5 +26,5 @@ minSeqNumber: 0, | ||
| }; | ||
| config.maxSeqNumber = options.maxSeqNumber && options.maxSeqNumber > 0 ? options.maxSeqNumber : (1 << config.seqBitLength) - 1; | ||
| config.minSeqNumber = options.minSeqNumber && options.minSeqNumber > 0 ? options.minSeqNumber : 5; | ||
| config.topOverCostCount = options.topOverCostCount && options.topOverCostCount > 0 ? options.topOverCostCount : 2e3; | ||
| config.maxSeqNumber = options.maxSeqNumber != null && options.maxSeqNumber > 0 ? options.maxSeqNumber : (1 << config.seqBitLength) - 1; | ||
| config.minSeqNumber = options.minSeqNumber != null && options.minSeqNumber >= 5 ? options.minSeqNumber : 5; | ||
| config.topOverCostCount = options.topOverCostCount != null && options.topOverCostCount > 0 ? options.topOverCostCount : 2e3; | ||
| return config; | ||
@@ -313,6 +313,14 @@ } | ||
| * 验证 ID 是否为当前配置下合法的 Snowflake ID | ||
| * @param strictWorkerId - 为 true 时要求 workerId 匹配当前实例 | ||
| * | ||
| * @param options - 校验选项,或传 boolean 表示 strictWorkerId(向后兼容) | ||
| * | ||
| * @example | ||
| * genid.isValid(id) // 宽松校验 | ||
| * genid.isValid(id, true) // 要求 workerId 匹配 | ||
| * genid.isValid(id, { strictWorkerId: true }) // 同上 | ||
| * genid.isValid(id, { afterTime: startupTime }) // 要求 ID 生成时间晚于 startupTime | ||
| */ | ||
| isValid(id, strictWorkerId = false) { | ||
| isValid(id, options = false) { | ||
| try { | ||
| const opts = typeof options === "boolean" ? { strictWorkerId: options } : options; | ||
| const idBigInt = BigInt(id); | ||
@@ -327,7 +335,6 @@ if (idBigInt < 0n) return false; | ||
| if (timestamp > BigInt(Date.now()) + 1000n) return false; | ||
| const maxWorkerId = (1n << this.workerIdBitLength) - 1n; | ||
| if (workerId < 0n || workerId > maxWorkerId) return false; | ||
| if (strictWorkerId && workerId !== this.workerId) return false; | ||
| const maxSeq = (1n << this.seqBitLength) - 1n; | ||
| if (sequence < 0n || sequence > maxSeq) return false; | ||
| if (opts.afterTime != null && timestamp < BigInt(opts.afterTime)) return false; | ||
| if (workerId > (1n << this.workerIdBitLength) - 1n) return false; | ||
| if (opts.strictWorkerId && workerId !== this.workerId) return false; | ||
| if (sequence > (1n << this.seqBitLength) - 1n) return false; | ||
| return true; | ||
@@ -334,0 +341,0 @@ } catch { |
+17
-3
@@ -67,2 +67,9 @@ //#region src/types.d.ts | ||
| } | ||
| /** isValid 校验选项 */ | ||
| interface ValidateOptions { | ||
| /** 为 true 时要求 workerId 匹配当前实例(默认:false) */ | ||
| strictWorkerId?: boolean; | ||
| /** ID 的生成时间不得早于此时间戳/毫秒(默认:baseTime) */ | ||
| afterTime?: number; | ||
| } | ||
| /** 配置信息 */ | ||
@@ -158,5 +165,12 @@ interface ConfigResult { | ||
| * 验证 ID 是否为当前配置下合法的 Snowflake ID | ||
| * @param strictWorkerId - 为 true 时要求 workerId 匹配当前实例 | ||
| * | ||
| * @param options - 校验选项,或传 boolean 表示 strictWorkerId(向后兼容) | ||
| * | ||
| * @example | ||
| * genid.isValid(id) // 宽松校验 | ||
| * genid.isValid(id, true) // 要求 workerId 匹配 | ||
| * genid.isValid(id, { strictWorkerId: true }) // 同上 | ||
| * genid.isValid(id, { afterTime: startupTime }) // 要求 ID 生成时间晚于 startupTime | ||
| */ | ||
| isValid(id: number | bigint | string, strictWorkerId?: boolean): boolean; | ||
| isValid(id: number | bigint | string, options?: boolean | ValidateOptions): boolean; | ||
| /** 将 ID 格式化为带标注的二进制字符串(调试用) */ | ||
@@ -166,2 +180,2 @@ formatBinary(id: number | bigint | string): string; | ||
| //#endregion | ||
| export { ConfigResult, GenidConfig, GenidMethod, GenidOptimized, GenidOptions, ParseResult, Stats, StatsResult }; | ||
| export { ConfigResult, GenidConfig, GenidMethod, GenidOptimized, GenidOptions, ParseResult, Stats, StatsResult, ValidateOptions }; |
+17
-3
@@ -67,2 +67,9 @@ //#region src/types.d.ts | ||
| } | ||
| /** isValid 校验选项 */ | ||
| interface ValidateOptions { | ||
| /** 为 true 时要求 workerId 匹配当前实例(默认:false) */ | ||
| strictWorkerId?: boolean; | ||
| /** ID 的生成时间不得早于此时间戳/毫秒(默认:baseTime) */ | ||
| afterTime?: number; | ||
| } | ||
| /** 配置信息 */ | ||
@@ -158,5 +165,12 @@ interface ConfigResult { | ||
| * 验证 ID 是否为当前配置下合法的 Snowflake ID | ||
| * @param strictWorkerId - 为 true 时要求 workerId 匹配当前实例 | ||
| * | ||
| * @param options - 校验选项,或传 boolean 表示 strictWorkerId(向后兼容) | ||
| * | ||
| * @example | ||
| * genid.isValid(id) // 宽松校验 | ||
| * genid.isValid(id, true) // 要求 workerId 匹配 | ||
| * genid.isValid(id, { strictWorkerId: true }) // 同上 | ||
| * genid.isValid(id, { afterTime: startupTime }) // 要求 ID 生成时间晚于 startupTime | ||
| */ | ||
| isValid(id: number | bigint | string, strictWorkerId?: boolean): boolean; | ||
| isValid(id: number | bigint | string, options?: boolean | ValidateOptions): boolean; | ||
| /** 将 ID 格式化为带标注的二进制字符串(调试用) */ | ||
@@ -166,2 +180,2 @@ formatBinary(id: number | bigint | string): string; | ||
| //#endregion | ||
| export { ConfigResult, GenidConfig, GenidMethod, GenidOptimized, GenidOptions, ParseResult, Stats, StatsResult }; | ||
| export { ConfigResult, GenidConfig, GenidMethod, GenidOptimized, GenidOptions, ParseResult, Stats, StatsResult, ValidateOptions }; |
+20
-13
@@ -17,5 +17,5 @@ //#region src/types.ts | ||
| method: options.method === GenidMethod.TRADITIONAL ? GenidMethod.TRADITIONAL : GenidMethod.DRIFT, | ||
| baseTime: options.baseTime && options.baseTime > 0 ? options.baseTime : (/* @__PURE__ */ new Date("2020-01-01")).valueOf(), | ||
| workerIdBitLength: options.workerIdBitLength && options.workerIdBitLength > 0 ? options.workerIdBitLength : 6, | ||
| seqBitLength: options.seqBitLength && options.seqBitLength > 0 ? options.seqBitLength : 6, | ||
| baseTime: options.baseTime != null && options.baseTime > 0 ? options.baseTime : (/* @__PURE__ */ new Date("2020-01-01")).valueOf(), | ||
| workerIdBitLength: options.workerIdBitLength != null && options.workerIdBitLength > 0 ? options.workerIdBitLength : 6, | ||
| seqBitLength: options.seqBitLength != null && options.seqBitLength > 0 ? options.seqBitLength : 6, | ||
| maxSeqNumber: 0, | ||
@@ -25,5 +25,5 @@ minSeqNumber: 0, | ||
| }; | ||
| config.maxSeqNumber = options.maxSeqNumber && options.maxSeqNumber > 0 ? options.maxSeqNumber : (1 << config.seqBitLength) - 1; | ||
| config.minSeqNumber = options.minSeqNumber && options.minSeqNumber > 0 ? options.minSeqNumber : 5; | ||
| config.topOverCostCount = options.topOverCostCount && options.topOverCostCount > 0 ? options.topOverCostCount : 2e3; | ||
| config.maxSeqNumber = options.maxSeqNumber != null && options.maxSeqNumber > 0 ? options.maxSeqNumber : (1 << config.seqBitLength) - 1; | ||
| config.minSeqNumber = options.minSeqNumber != null && options.minSeqNumber >= 5 ? options.minSeqNumber : 5; | ||
| config.topOverCostCount = options.topOverCostCount != null && options.topOverCostCount > 0 ? options.topOverCostCount : 2e3; | ||
| return config; | ||
@@ -312,6 +312,14 @@ } | ||
| * 验证 ID 是否为当前配置下合法的 Snowflake ID | ||
| * @param strictWorkerId - 为 true 时要求 workerId 匹配当前实例 | ||
| * | ||
| * @param options - 校验选项,或传 boolean 表示 strictWorkerId(向后兼容) | ||
| * | ||
| * @example | ||
| * genid.isValid(id) // 宽松校验 | ||
| * genid.isValid(id, true) // 要求 workerId 匹配 | ||
| * genid.isValid(id, { strictWorkerId: true }) // 同上 | ||
| * genid.isValid(id, { afterTime: startupTime }) // 要求 ID 生成时间晚于 startupTime | ||
| */ | ||
| isValid(id, strictWorkerId = false) { | ||
| isValid(id, options = false) { | ||
| try { | ||
| const opts = typeof options === "boolean" ? { strictWorkerId: options } : options; | ||
| const idBigInt = BigInt(id); | ||
@@ -326,7 +334,6 @@ if (idBigInt < 0n) return false; | ||
| if (timestamp > BigInt(Date.now()) + 1000n) return false; | ||
| const maxWorkerId = (1n << this.workerIdBitLength) - 1n; | ||
| if (workerId < 0n || workerId > maxWorkerId) return false; | ||
| if (strictWorkerId && workerId !== this.workerId) return false; | ||
| const maxSeq = (1n << this.seqBitLength) - 1n; | ||
| if (sequence < 0n || sequence > maxSeq) return false; | ||
| if (opts.afterTime != null && timestamp < BigInt(opts.afterTime)) return false; | ||
| if (workerId > (1n << this.workerIdBitLength) - 1n) return false; | ||
| if (opts.strictWorkerId && workerId !== this.workerId) return false; | ||
| if (sequence > (1n << this.seqBitLength) - 1n) return false; | ||
| return true; | ||
@@ -333,0 +340,0 @@ } catch { |
+7
-9
| { | ||
| "name": "@cdlab996/genid", | ||
| "type": "module", | ||
| "version": "1.3.0", | ||
| "description": "基于 Snowflake 算法的高性能分布式唯一 ID 生成器", | ||
| "version": "1.4.0", | ||
| "description": "High-performance distributed unique ID generator based on the Snowflake algorithm", | ||
| "author": "wudi <wuchendi96@gmail.com>", | ||
@@ -50,7 +50,7 @@ "license": "MIT", | ||
| "devDependencies": { | ||
| "@biomejs/biome": "^2.4.7", | ||
| "@biomejs/biome": "^2.4.10", | ||
| "@types/node": "^25", | ||
| "tsdown": "^0.21.4", | ||
| "typescript": "^5", | ||
| "vitest": "^4.1.0" | ||
| "tsdown": "^0.21.7", | ||
| "typescript": "^6", | ||
| "vitest": "^4.1.2" | ||
| }, | ||
@@ -65,7 +65,5 @@ "publishConfig": { | ||
| "test": "vitest --run", | ||
| "test:index": "vitest --run tests/index.test.ts", | ||
| "test:performance": "vitest --run tests/performance.test.ts", | ||
| "test:stress": "vitest --run tests/stress.test.ts", | ||
| "lint": "biome check", | ||
| "format": "biome format --write", | ||
| "benchmark": "npx tsx scripts/benchmark.ts", | ||
| "clean": "bash ./scripts/clean.sh", | ||
@@ -72,0 +70,0 @@ "typecheck": "tsc --project tsconfig.json" |
+227
-101
@@ -6,14 +6,16 @@ # @cdlab996/genid | ||
| 基于 Snowflake 算法的高性能分布式唯一 ID 生成器,支持漂移算法和时钟回拨处理。 | ||
| High-performance distributed unique ID generator based on the Snowflake algorithm, with drift mode and clock-rollback handling. | ||
| ## 特性 | ||
| [中文文档](./README.zh-CN.md) | ||
| - **漂移算法** - 高并发场景下突破每毫秒序列号上限,性能更优 | ||
| - **时钟回拨处理** - 使用保留序列号优雅降级,不阻塞 ID 生成 | ||
| - **灵活配置** - 支持自定义时间戳、节点 ID、序列号的位长度分配 | ||
| - **ID 验证** - 支持严格/宽松模式校验 ID 有效性 | ||
| - **运行监控** - 内置统计、解析和二进制格式化调试工具 | ||
| ## Features | ||
| ## 安装 | ||
| - **Drift Algorithm** - Exceeds per-millisecond sequence limits under high concurrency for better throughput | ||
| - **Clock Rollback Handling** - Graceful degradation using reserved sequence numbers without blocking ID generation | ||
| - **Flexible Configuration** - Customize bit allocation for timestamp, worker ID, and sequence | ||
| - **ID Validation** - Strict and loose validation modes with `afterTime` support | ||
| - **Runtime Monitoring** - Built-in statistics, parsing, and binary formatting for debugging | ||
| ## Installation | ||
| ```bash | ||
@@ -27,3 +29,3 @@ # npm | ||
| ## 快速开始 | ||
| ## Quick Start | ||
@@ -33,16 +35,16 @@ ```typescript | ||
| // 创建实例(每个 Worker/进程使用不同的 workerId) | ||
| // Create an instance (use a different workerId for each worker/process) | ||
| const genid = new GenidOptimized({ workerId: 1 }) | ||
| // 生成 ID | ||
| // Generate an ID | ||
| const id = genid.nextId() | ||
| // 批量生成 | ||
| // Batch generate | ||
| const ids = genid.nextBatch(1000) | ||
| // 解析 ID | ||
| // Parse an ID | ||
| const info = genid.parse(id) | ||
| // => { timestamp: Date, timestampMs: 1609459200000, workerId: 1, sequence: 42 } | ||
| // 验证 ID | ||
| // Validate an ID | ||
| genid.isValid(id) // true | ||
@@ -55,44 +57,48 @@ ``` | ||
| | 参数 | 类型 | 必填 | 默认值 | 说明 | | ||
| | ------------------- | ------------- | :---: | ------------------ | --------------------------------------------- | | ||
| | `workerId` | `number` | Yes | - | 工作节点 ID(0 ~ 2^workerIdBitLength-1) | | ||
| | `method` | `GenidMethod` | | `DRIFT` | 算法:`DRIFT`(漂移)或 `TRADITIONAL`(传统) | | ||
| | `baseTime` | `number` | | `1577836800000` | 起始时间戳,毫秒(默认 2020-01-01) | | ||
| | `workerIdBitLength` | `number` | | `6` | 节点 ID 位数(1-15) | | ||
| | `seqBitLength` | `number` | | `6` | 序列号位数(3-21) | | ||
| | `maxSeqNumber` | `number` | | `2^seqBitLength-1` | 最大序列号 | | ||
| | `minSeqNumber` | `number` | | `5` | 最小序列号(0-4 保留用于时钟回拨) | | ||
| | `topOverCostCount` | `number` | | `2000` | 最大漂移次数 | | ||
| | Parameter | Type | Required | Default | Description | | ||
| | ------------------- | ------------- | :------: | ------------------ | --------------------------------------------------- | | ||
| | `workerId` | `number` | Yes | - | Worker node ID (0 to 2^workerIdBitLength-1) | | ||
| | `method` | `GenidMethod` | | `DRIFT` | Algorithm: `DRIFT` or `TRADITIONAL` | | ||
| | `baseTime` | `number` | | `1577836800000` | Base timestamp in ms (default: 2020-01-01) | | ||
| | `workerIdBitLength` | `number` | | `6` | Bit length for worker ID (1-15) | | ||
| | `seqBitLength` | `number` | | `6` | Bit length for sequence (3-21) | | ||
| | `maxSeqNumber` | `number` | | `2^seqBitLength-1` | Maximum sequence number | | ||
| | `minSeqNumber` | `number` | | `5` | Minimum sequence number (0-4 reserved for rollback) | | ||
| | `topOverCostCount` | `number` | | `2000` | Maximum drift count | | ||
| ### 生成 ID | ||
| ### Generating IDs | ||
| ```typescript | ||
| genid.nextId() // 返回 number | bigint(自动选择) | ||
| genid.nextNumber() // 返回 number(超出安全整数范围抛错) | ||
| genid.nextBigId() // 返回 bigint | ||
| genid.nextBatch(100) // 批量生成 100 个 ID | ||
| genid.nextBatch(100, true) // 批量生成 100 个 BigInt ID | ||
| genid.nextId() // Returns number | bigint (auto-selects) | ||
| genid.nextNumber() // Returns number (throws if exceeds safe integer range) | ||
| genid.nextBigId() // Returns bigint | ||
| genid.nextBatch(100) // Batch generate 100 IDs | ||
| genid.nextBatch(100, true) // Batch generate 100 BigInt IDs | ||
| ``` | ||
| ### 解析与验证 | ||
| ### Parsing & Validation | ||
| ```typescript | ||
| // 解析 ID 的组成部分 | ||
| // Parse an ID into its components | ||
| genid.parse(id) | ||
| // => { timestamp: Date, timestampMs: number, workerId: number, sequence: number } | ||
| // 宽松验证:检查 ID 格式是否有效 | ||
| genid.isValid(id) // true | ||
| genid.isValid(12345) // false | ||
| genid.isValid('invalid') // false | ||
| // Loose validation: checks structural validity | ||
| genid.isValid(id) // true | ||
| genid.isValid('invalid') // false | ||
| // 严格验证:要求 workerId 匹配当前实例 | ||
| genid.isValid(id, true) // true(本实例生成的 ID) | ||
| genid.isValid(otherId, true) // false(其他实例生成的 ID) | ||
| // Strict validation: requires workerId to match the current instance | ||
| genid.isValid(id, true) // true (generated by this instance) | ||
| genid.isValid(otherId, true) // false (generated by another instance) | ||
| // Time-bound validation: reject IDs generated before a given time | ||
| const startupTime = Date.now() | ||
| genid.isValid(id, { afterTime: startupTime }) // true | ||
| genid.isValid(id, { strictWorkerId: true, afterTime: startupTime }) // combined | ||
| ``` | ||
| ### 统计与配置 | ||
| ### Statistics & Configuration | ||
| ```typescript | ||
| // 获取运行统计 | ||
| // Get runtime statistics | ||
| genid.getStats() | ||
@@ -108,3 +114,3 @@ // => { | ||
| // 获取当前配置 | ||
| // Get current configuration | ||
| genid.getConfig() | ||
@@ -124,7 +130,7 @@ // => { | ||
| // 重置统计 | ||
| // Reset statistics | ||
| genid.resetStats() | ||
| ``` | ||
| ### 调试 | ||
| ### Debugging | ||
@@ -135,10 +141,10 @@ ```typescript | ||
| // Binary (64-bit): | ||
| // 0000000000011010... - 时间戳 (52 bits) = 2025-10-17T... | ||
| // 000001 - 工作节点 ID (6 bits) = 1 | ||
| // 101010 - 序列号 (6 bits) = 42 | ||
| // 0000000000011010... - Timestamp (52 bits) = 2025-10-17T... | ||
| // 000001 - Worker ID (6 bits) = 1 | ||
| // 101010 - Sequence (6 bits) = 42 | ||
| ``` | ||
| ## 使用示例 | ||
| ## Examples | ||
| ### 自定义位分配 | ||
| ### Custom Bit Allocation | ||
@@ -152,4 +158,4 @@ ```typescript | ||
| baseTime: new Date('2024-01-01').valueOf(), | ||
| workerIdBitLength: 10, // 支持 1024 个节点 | ||
| seqBitLength: 12, // 每毫秒 4096 个 ID | ||
| workerIdBitLength: 10, // Support 1024 nodes | ||
| seqBitLength: 12, // 4096 IDs per millisecond | ||
| topOverCostCount: 5000, | ||
@@ -159,17 +165,17 @@ }) | ||
| ### 验证外部 ID | ||
| ### Validating External IDs | ||
| ```typescript | ||
| // 验证从数据库或 API 获取的 ID | ||
| // Validate IDs from a database or API | ||
| const externalId = '123456789012345' | ||
| if (genid.isValid(externalId)) { | ||
| const info = genid.parse(externalId) | ||
| console.log('生成时间:', info.timestamp) | ||
| console.log('来自节点:', info.workerId) | ||
| console.log('Generated at:', info.timestamp) | ||
| console.log('From worker:', info.workerId) | ||
| } else { | ||
| console.error('无效 ID') | ||
| console.error('Invalid ID') | ||
| } | ||
| ``` | ||
| ### 性能监控 | ||
| ### Performance Monitoring | ||
@@ -179,52 +185,52 @@ ```typescript | ||
| const stats = genid.getStats() | ||
| console.log(`速率: ${stats.avgPerSecond} ID/s | 漂移: ${stats.overCostCount} | 回拨: ${stats.turnBackCount}`) | ||
| console.log(`Rate: ${stats.avgPerSecond} ID/s | Drift: ${stats.overCostCount} | Rollback: ${stats.turnBackCount}`) | ||
| }, 10000) | ||
| ``` | ||
| ## 算法模式 | ||
| ## Algorithm Modes | ||
| | 模式 | 说明 | 适用场景 | | ||
| | ----------------- | ---------------------------------------- | -------------------- | | ||
| | **DRIFT**(默认) | 序列号耗尽时借用未来时间戳,避免等待 | 高频 ID 生成、高并发 | | ||
| | **TRADITIONAL** | 严格按时间戳递增,序列号耗尽等待下一毫秒 | 对时间顺序严格要求 | | ||
| | Mode | Description | Use Case | | ||
| | ------------------- | ------------------------------------------------------------------ | ----------------------------- | | ||
| | **DRIFT** (default) | Borrows future timestamps when sequence is exhausted; avoids waits | High-frequency ID generation | | ||
| | **TRADITIONAL** | Strictly increasing timestamps; waits for next ms on exhaustion | Strict time-ordering required | | ||
| ## 架构 | ||
| ## Architecture | ||
| ### ID 结构(64-bit) | ||
| ### ID Structure (64-bit) | ||
| ``` | ||
| |------------ 时间戳 ------------|-- 工作节点 ID --|-- 序列号 --| | ||
| 42-52 bits 1-15 bits 3-21 bits | ||
| |------------ Timestamp ------------|-- Worker ID --|-- Sequence --| | ||
| 42-52 bits 1-15 bits 3-21 bits | ||
| ``` | ||
| 默认配置:时间戳 52 bits(约 139 年)| 节点 ID 6 bits(64 个节点)| 序列号 6 bits(每毫秒 59 个 ID) | ||
| Default: Timestamp 52 bits (~139 years) | Worker ID 6 bits (64 nodes) | Sequence 6 bits (59 IDs/ms) | ||
| 序列号 `0-4` 保留用于时钟回拨,正常使用从 `5` 开始。 | ||
| Sequence values `0-4` are reserved for clock rollback; normal generation starts at `5`. | ||
| ### 核心流程 | ||
| ### Core Flow | ||
| ```mermaid | ||
| graph TB | ||
| A[开始生成 ID] --> B{是否处于漂移状态?} | ||
| A[Start ID Generation] --> B{In drift mode?} | ||
| B -->|否| C[正常路径] | ||
| B -->|是| D[漂移路径] | ||
| B -->|No| C[Normal Path] | ||
| B -->|Yes| D[Drift Path] | ||
| C --> E{检测时钟} | ||
| E -->|时钟回拨| F[使用保留序列号 0-4] | ||
| E -->|时间前进| G[重置序列号] | ||
| E -->|同一毫秒| H{序列号是否溢出?} | ||
| C --> E{Check Clock} | ||
| E -->|Clock Rollback| F[Use Reserved Sequence 0-4] | ||
| E -->|Time Advanced| G[Reset Sequence] | ||
| E -->|Same Millisecond| H{Sequence Exhausted?} | ||
| H -->|否| I[序列号+1 正常生成] | ||
| H -->|是| J[进入漂移状态 时间戳+1] | ||
| H -->|No| I[Increment Sequence] | ||
| H -->|Yes| J[Enter Drift Mode, Timestamp+1] | ||
| D --> K{检测时间} | ||
| K -->|时间追上| L[退出漂移 恢复正常] | ||
| K -->|超过最大漂移| M[等待下一毫秒 退出漂移] | ||
| K -->|继续漂移| N{序列号是否溢出?} | ||
| D --> K{Check Time} | ||
| K -->|Time Caught Up| L[Exit Drift, Resume Normal] | ||
| K -->|Exceeded Max Drift| M[Wait for Next ms, Exit Drift] | ||
| K -->|Continue Drift| N{Sequence Exhausted?} | ||
| N -->|否| O[使用当前序列号] | ||
| N -->|是| P[时间戳+1 重置序列号] | ||
| N -->|No| O[Use Current Sequence] | ||
| N -->|Yes| P[Timestamp+1, Reset Sequence] | ||
| F --> Q[计算 ID] | ||
| F --> Q[Assemble ID] | ||
| G --> Q | ||
@@ -238,25 +244,145 @@ I --> Q | ||
| Q --> R[更新统计] | ||
| R --> S[返回 ID] | ||
| Q --> R[Update Statistics] | ||
| R --> S[Return ID] | ||
| ``` | ||
| ## 注意事项 | ||
| ## Performance | ||
| - 每个 Worker/进程必须使用**不同的 workerId** | ||
| - 实例**非线程安全**,不要跨线程共享 | ||
| - `workerIdBitLength + seqBitLength` 不能超过 22 | ||
| - 序列号 0-4 保留用于时钟回拨处理 | ||
| - 超出 JavaScript 安全整数范围(2^53-1)时,使用 `nextBigId()` 或 `nextId()`(自动返回 BigInt) | ||
| Throughput is determined by the number of available sequence slots per millisecond. Increasing `seqBitLength` scales linearly: | ||
| ## 性能 | ||
| | seqBitLength | Slots/ms | Throughput | | ||
| | :-------------: | -------: | ------------------: | | ||
| | 3 | 3 | ~3,000 IDs/sec | | ||
| | 4 | 11 | ~11,000 IDs/sec | | ||
| | **6** (default) | **59** | **~58,000 IDs/sec** | | ||
| | 8 | 251 | ~247,000 IDs/sec | | ||
| | 10 | 1,019 | ~1,000,000 IDs/sec | | ||
| | 14 | 16,379 | ~4,500,000 IDs/sec | | ||
| | 指标 | 数值 | | ||
| | -------------------------- | ------------- | | ||
| | 单实例吞吐量 | > 50,000 ID/s | | ||
| | 每毫秒生成量(默认配置) | 59 个 | | ||
| | 最大节点数(默认配置) | 64 个 | | ||
| | 时间戳可用时长(默认配置) | ~139 年 | | ||
| > Measured on Node.js v22 (x64). Actual results vary by environment. | ||
| > Run `pnpm run benchmark` to probe your own machine. | ||
| | Metric | Value (default config) | | ||
| | ------------------------- | ---------------------: | | ||
| | Max worker nodes | 64 | | ||
| | Timestamp lifespan | ~139 years | | ||
| | P99 latency (single call) | < 1µs | | ||
| ## Benchmark | ||
| A built-in probe script measures the actual capability of the current environment: | ||
| ```bash | ||
| pnpm run benchmark | ||
| ``` | ||
| It reports: | ||
| 1. **Single-call throughput** — peak `nextId()` IDs/sec | ||
| 2. **Batch throughput** — `nextBatch()` across different batch sizes | ||
| 3. **Latency percentiles** — P50 / P95 / P99 / P99.9 / Max | ||
| 4. **Algorithm comparison** — DRIFT vs TRADITIONAL side-by-side | ||
| 5. **Throughput by seqBitLength** — how bit allocation affects throughput | ||
| 6. **Memory footprint** — heap delta after 1M generations | ||
| 7. **Recommended thresholds** — suggested safe values for test assertions (60% of peak) | ||
| <details> | ||
| <summary>Example output</summary> | ||
| ```bash | ||
| station :: /app/projects/genid ‹main*› » pnpm run benchmark | ||
| > @cdlab996/genid@1.4.0 benchmark /app/projects/genid | ||
| > npx tsx scripts/benchmark.ts | ||
| ============================================================ | ||
| GenidOptimized — Environment Capability Probe | ||
| Node v22.22.0 | linux x64 | ||
| Date: 2026-04-01T08:41:07.669Z | ||
| ============================================================ | ||
| ──────────────────────────────────────────────────────────── | ||
| 1. Single-call throughput (nextId) | ||
| ──────────────────────────────────────────────────────────── | ||
| Duration: 3001ms | ||
| Generated: 226,073 | ||
| Throughput: 75,332 IDs/sec | ||
| ──────────────────────────────────────────────────────────── | ||
| 2. Batch throughput (nextBatch) | ||
| ──────────────────────────────────────────────────────────── | ||
| batch= 100 × 5000 => 61,665 IDs/sec | ||
| batch= 1,000 × 500 => 61,577 IDs/sec | ||
| batch= 10,000 × 50 => 61,716 IDs/sec | ||
| batch=100,000 × 5 => 61,644 IDs/sec | ||
| ──────────────────────────────────────────────────────────── | ||
| 3. Single-call latency percentiles | ||
| ──────────────────────────────────────────────────────────── | ||
| Samples: 100,000 | ||
| Avg: 0µs | ||
| P50: 0µs | ||
| P95: 1µs | ||
| P99: 1µs | ||
| P99.9: 1µs | ||
| Max: 364µs | ||
| ──────────────────────────────────────────────────────────── | ||
| 4. Algorithm comparison (DRIFT vs TRADITIONAL) | ||
| ──────────────────────────────────────────────────────────── | ||
| DRIFT 58,296 IDs/sec drift=1 | ||
| TRADITIONAL 59,000 IDs/sec drift=0 | ||
| ──────────────────────────────────────────────────────────── | ||
| 5. Throughput by sequence bit length | ||
| ──────────────────────────────────────────────────────────── | ||
| seqBits= 3 maxSeq= 7 => 2,986 IDs/sec drift=1 | ||
| seqBits= 4 maxSeq= 15 => 10,884 IDs/sec drift=1 | ||
| seqBits= 6 maxSeq= 63 => 58,240 IDs/sec drift=1 | ||
| seqBits= 8 maxSeq= 255 => 247,804 IDs/sec drift=1 | ||
| seqBits=10 maxSeq= 1023 => 1,008,930 IDs/sec drift=1 | ||
| seqBits=14 maxSeq=16383 => 4,130,701 IDs/sec drift=0 | ||
| ──────────────────────────────────────────────────────────── | ||
| 6. Memory footprint | ||
| ──────────────────────────────────────────────────────────── | ||
| Generated: 1,000,000 IDs (not stored) | ||
| Heap delta: -6.27 MB | ||
| Note: Run with --expose-gc for accurate GC-forced measurement | ||
| ──────────────────────────────────────────────────────────── | ||
| Summary — Recommended test thresholds | ||
| ──────────────────────────────────────────────────────────── | ||
| Peak single-call: 75,332 IDs/sec | ||
| Peak batch: 61,716 IDs/sec | ||
| Suggested min threshold: 45,199 IDs/sec (60% of peak) | ||
| Suggested batch threshold: 37,029 IDs/sec (60% of peak) | ||
| Suggested P99 cap: 2µs (3× measured P99) | ||
| ``` | ||
| </details> | ||
| ## Development | ||
| ```bash | ||
| pnpm install # Install dependencies | ||
| pnpm run build # Build (ESM + CJS) | ||
| pnpm run dev # Watch mode | ||
| pnpm run test # Run tests | ||
| pnpm run benchmark # Run environment capability probe | ||
| pnpm run typecheck # Type check | ||
| pnpm run lint # Lint (Biome) | ||
| pnpm run format # Format (Biome) | ||
| ``` | ||
| ## Notes | ||
| - Each worker/process must use a **unique workerId** | ||
| - Instances are **not thread-safe** — do not share across threads | ||
| - `workerIdBitLength + seqBitLength` must not exceed 22 | ||
| - Sequence values 0-4 are reserved for clock-rollback handling | ||
| - When IDs exceed the JavaScript safe integer range (2^53-1), use `nextBigId()` or `nextId()` (auto-returns BigInt) | ||
| ## License | ||
| [MIT](./LICENSE) License © 2025-PRESENT [wudi](https://github.com/WuChenDi) |
72161
47.2%8
14.29%720
1.98%378
50%