perf(http): 修复响应日期格式化每次重建 ICU formatter 导致的 CPU 热点

formatResponseDateTime 在函数内部 `new Intl.DateTimeFormat(...)`,而
buildSuccessPayload 会对每个成功响应的每个日期字段调用它。ICU formatter
单次构造约 0.5~0.8ms,V8 CPU 采样显示该函数占后端主线程约 30% CPU(约 0.33 核)。

改动:
- formatter 提到模块级只构造一次;formatToParts 直取字段,去掉
  Object.fromEntries + filter + map 的三次分配
- isIsoDateTimeString 增加 O(1) 位置预判,避免对每个字段值跑正则
- normalizeResponseDateTime 用显式循环替代 entries/fromEntries 双重分配
- 同类问题一并修复:open-91 formatOpen91ExpireTime、redeem formatGrantTime

输出逐字节一致:新增等价性测试,以修复前实现为参考覆盖跨时区/毫秒/
闰日/跨年/非法日期语料。同形态响应(200 行 × 6 日期字段)实测
35.2ms → 2.6ms(13.6x)。
This commit is contained in:
yml2213
2026-10-30 21:20:11 +08:00
parent fb485f7e36
commit 14b595b0db
4 changed files with 246 additions and 48 deletions
+20 -11
View File
@@ -3,6 +3,21 @@ import { OPEN_91_DEFAULT_FAIL_CODE, OPEN_91_SUCCESS_MESSAGE } from './config.js'
import { isOpen91FailedTaskStatus } from '../../domain/task-status.js'
import { normalizeOpen91String } from './payload.js'
/**
* 模块级只构造一次:`new Intl.DateTimeFormat()` 每次都会重新装载 ICU locale 数据,
* 单次约 0.5ms。该格式化器按响应字段逐条调用,放在函数内会随响应行数线性放大。
*/
const OPEN_91_EXPIRE_FORMATTER = new Intl.DateTimeFormat('zh-CN', {
timeZone: 'Asia/Shanghai',
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hourCycle: 'h23',
})
export function buildOpen91SuccessResponse(data: unknown, message = OPEN_91_SUCCESS_MESSAGE) {
return {
code: 200,
@@ -57,18 +72,12 @@ export function formatOpen91ExpireTime(value: unknown = '') {
return normalized
}
const parts = new Intl.DateTimeFormat('zh-CN', {
timeZone: 'Asia/Shanghai',
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hourCycle: 'h23',
}).formatToParts(date)
const parts = OPEN_91_EXPIRE_FORMATTER.formatToParts(date)
const byType = Object.fromEntries(parts.map((part) => [part.type, part.value]))
const byType: Record<string, string> = {}
for (const part of parts) {
if (part.type !== 'literal') byType[part.type] = part.value
}
return `${byType.year}-${byType.month}-${byType.day} ${byType.hour}:${byType.minute}:${byType.second}`
}
@@ -842,6 +842,20 @@ export async function findCertificateOrThrow(certNo: string): Promise<RedeemCert
return row
}
/**
* 模块级只构造一次:`new Intl.DateTimeFormat()` 每次都会重新装载 ICU locale 数据,
* 单次约 0.5ms。凭证按条签发/查询时会逐条调用 formatGrantTime,放在函数内会线性放大。
*/
const GRANT_TIME_FORMATTER = new Intl.DateTimeFormat('zh-CN', {
timeZone: 'Asia/Shanghai',
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
})
/** 平台的 grant_time 形如 `YYYY-MM-DD HH:MM:SS`(北京时间)。 */
export function formatGrantTime(iso: string): string {
const date = new Date(iso)
@@ -849,18 +863,12 @@ export function formatGrantTime(iso: string): string {
return ''
}
const parts = new Intl.DateTimeFormat('zh-CN', {
timeZone: 'Asia/Shanghai',
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
}).formatToParts(date)
const parts = GRANT_TIME_FORMATTER.formatToParts(date)
const pick = (type: Intl.DateTimeFormatPartTypes) =>
parts.find((part) => part.type === type)?.value || ''
const byType: Record<string, string> = {}
for (const part of parts) {
if (part.type !== 'literal') byType[part.type] = part.value
}
return `${pick('year')}-${pick('month')}-${pick('day')} ${pick('hour')}:${pick('minute')}:${pick('second')}`
return `${byType.year}-${byType.month}-${byType.day} ${byType.hour}:${byType.minute}:${byType.second}`
}