Skip to content

Commit fe4540e

Browse files
araujoguisxa
authored andcommitted
util: create hex style cache and fast path
Signed-off-by: Guilherme Araújo <arauujogui@gmail.com> PR-URL: #62999 Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day> Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Ethan Arrowood <ethan@arrowood.dev>
1 parent e4e5137 commit fe4540e

2 files changed

Lines changed: 77 additions & 24 deletions

File tree

‎benchmark/util/style-text.js‎

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,22 @@ const common = require('../common.js');
55
const{ styleText }=require('node:util');
66
constassert=require('node:assert');
77

8+
// 1000 distinct hex colors to exercise the cache under high-miss conditions.
9+
// Spread evenly across hue space so colors are valid and maximally varied.
10+
constkHexColorCount=1000;
11+
consttoHex=(n)=>n.toString(16).padStart(2,'0');
12+
consthexColors=Array.from({length: kHexColorCount},(_,i)=>{
13+
constr=(i*37)&0xff;
14+
constg=(i*73)&0xff;
15+
constb=(i*137)&0xff;
16+
return`#${toHex(r)}${toHex(g)}${toHex(b)}`;
17+
});
18+
819
constbench=common.createBenchmark(main,{
920
messageType: ['string','number','boolean','invalid'],
10-
format: ['red','italic','invalid','#ff0000'],
21+
// '#rotating' cycles through kHexColorCount distinct colors to simulate
22+
// the high-miss-rate / large-cache scenario (e.g. user-randomised colors).
23+
format: ['red','italic','invalid','#ff0000','#rotating'],
1124
validateStream: [1,0],
1225
n: [1e3],
1326
});
@@ -31,9 +44,10 @@ function main({ messageType, format, validateStream, n }) {
3144

3245
bench.start();
3346
for(leti=0;i<n;i++){
47+
constfmt=format==='#rotating' ? hexColors[i%kHexColorCount] : format;
3448
letcolored='';
3549
try{
36-
colored=styleText(format,str,{ validateStream });
50+
colored=styleText(fmt,str,{ validateStream });
3751
assert.ok(colored);// Attempt to avoid dead-code elimination
3852
}catch{
3953
// eslint-disable no-empty

‎lib/util.js‎

Lines changed: 61 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ const {
3838
ObjectValues,
3939
ReflectApply,
4040
RegExpPrototypeExec,
41+
SafeMap,
4142
StringPrototypeSlice,
4243
StringPrototypeToWellFormed,
4344
}=primordials;
@@ -114,8 +115,20 @@ const kEscapeEnd = 'm';
114115
constkDimCode=2;
115116
constkBoldCode=1;
116117

118+
// Close sequence for 24-bit foreground colors (reset to default foreground)
119+
constkHexCloseSeq=kEscape+'39'+kEscapeEnd;
120+
117121
letstyleCache;
118122

123+
constkHexStyleCacheMax=256;
124+
125+
lethexStyleCache;
126+
127+
functiongetHexStyleCache(){
128+
hexStyleCache??=newSafeMap();
129+
returnhexStyleCache;
130+
}
131+
119132
functiongetStyleCache(){
120133
if(styleCache===undefined){
121134
styleCache={__proto__: null};
@@ -137,6 +150,28 @@ function getStyleCache() {
137150
returnstyleCache;
138151
}
139152

153+
/**
154+
* Returns the cached ANSI escape sequences for a hex color.
155+
* Computes and caches on first use to avoid repeated Buffer allocations.
156+
* @param {string} hex A valid hex color string (#RGB or #RRGGBB)
157+
* @returns {{openSeq: string, closeSeq: string}}
158+
*/
159+
functiongetHexStyle(hex){
160+
constcache=getHexStyleCache();
161+
constcached=cache.get(hex);
162+
if(cached!==undefined)returncached;
163+
const{0: r,1: g,2: b}=hexToRgb(hex);
164+
conststyle={
165+
__proto__: null,
166+
openSeq: kEscape+rgbToAnsi24Bit(r,g,b)+kEscapeEnd,
167+
closeSeq: kHexCloseSeq,
168+
};
169+
if(cache.size>=kHexStyleCacheMax)
170+
cache.delete(cache.keys().next().value);
171+
cache.set(hex,style);
172+
returnstyle;
173+
}
174+
140175
functionreplaceCloseCode(str,closeSeq,openSeq,keepClose){
141176
constcloseLen=closeSeq.length;
142177
letindex=str.indexOf(closeSeq);
@@ -163,15 +198,6 @@ function replaceCloseCode(str, closeSeq, openSeq, keepClose) {
163198
// Matches #RGB or #RRGGBB
164199
consthexColorRegExp=/^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
165200

166-
/**
167-
* Validates whether a string is a valid hex color code.
168-
* @param {string} hex The hex string to validate (e.g., '#fff' or '#ffffff')
169-
* @returns {boolean} True if valid hex color, false otherwise
170-
*/
171-
functionisValidHexColor(hex){
172-
returntypeofhex==='string'&&RegExpPrototypeExec(hexColorRegExp,hex)!==null;
173-
}
174-
175201
/**
176202
* Parses a hex color string into RGB components.
177203
* Supports both 3-digit (#RGB) and 6-digit (#RRGGBB) formats.
@@ -225,6 +251,17 @@ function styleText(format, text, options) {
225251
constprocessed=replaceCloseCode(text,style.closeSeq,style.openSeq,style.keepClose);
226252
returnstyle.openSeq+processed+style.closeSeq;
227253
}
254+
255+
if(format[0]==='#'){
256+
lethexStyle=getHexStyleCache().get(format);
257+
if(hexStyle===undefined&&RegExpPrototypeExec(hexColorRegExp,format)!==null){
258+
hexStyle=getHexStyle(format);
259+
}
260+
if(hexStyle!==undefined){
261+
constprocessed=replaceCloseCode(text,hexStyle.closeSeq,hexStyle.openSeq,false);
262+
returnhexStyle.openSeq+processed+hexStyle.closeSeq;
263+
}
264+
}
228265
}
229266

230267
validateString(text,'text');
@@ -255,24 +292,26 @@ function styleText(format, text, options) {
255292
for(constkeyofformatArray){
256293
if(key==='none')continue;
257294

258-
if(isValidHexColor(key)){
259-
if(skipColorize)continue;
260-
const{0: r,1: g,2: b}=hexToRgb(key);
261-
constopenSeq=kEscape+rgbToAnsi24Bit(r,g,b)+kEscapeEnd;
262-
constcloseSeq=kEscape+'39'+kEscapeEnd;
263-
openCodes+=openSeq;
264-
closeCodes=closeSeq+closeCodes;
265-
processedText=replaceCloseCode(processedText,closeSeq,openSeq,false);
295+
if(typeofkey==='string'&&key[0]==='#'){
296+
lethexStyle=getHexStyleCache().get(key);
297+
if(hexStyle===undefined){
298+
if(RegExpPrototypeExec(hexColorRegExp,key)===null){
299+
thrownewERR_INVALID_ARG_VALUE('format',key,
300+
'must be a valid hex color (#RGB or #RRGGBB)');
301+
}
302+
if(skipColorize)continue;
303+
hexStyle=getHexStyle(key);
304+
}elseif(skipColorize){
305+
continue;
306+
}
307+
openCodes+=hexStyle.openSeq;
308+
closeCodes=hexStyle.closeSeq+closeCodes;
309+
processedText=replaceCloseCode(processedText,hexStyle.closeSeq,hexStyle.openSeq,false);
266310
continue;
267311
}
268312

269313
conststyle=cache[key];
270314
if(style===undefined){
271-
// Check if it looks like an invalid hex color (starts with #)
272-
if(typeofkey==='string'&&key[0]==='#'){
273-
thrownewERR_INVALID_ARG_VALUE('format',key,
274-
'must be a valid hex color (#RGB or #RRGGBB)');
275-
}
276315
validateOneOf(key,'format',ObjectGetOwnPropertyNames(inspect.colors));
277316
}
278317
openCodes+=style.openSeq;

0 commit comments

Comments
 (0)