ntk 8.12.4 → 8.12.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -63,6 +63,60 @@ function shapingKeyOf(style) {
63
63
  return `${feats}|${language ?? ''}|${letterSpacing || 0}`;
64
64
  }
65
65
 
66
+ /**
67
+ * How many shaped words a generation of the memo holds. Two are kept, so a
68
+ * working set up to twice this survives a relayout; what was asked for in
69
+ * neither generation is dropped when the current one fills.
70
+ */
71
+ const SHAPE_GENERATION = 4000;
72
+
73
+ // The part of a shaping key a style decides, per style object. Built once a
74
+ // span rather than once a word: TextLayout asks for every word of a span with
75
+ // the same object, and the 2D context builds one object per `font` it is
76
+ // given. Neither changes an object after handing it over, so the only field
77
+ // checked again is the resolved face, the one a caller sets after building
78
+ // the rest.
79
+ const shapePrefixes = new WeakMap();
80
+
81
+ /** The key a style's words are memoized under, with the embedding levels
82
+ * they were shaped at: one string per style and level, kept, so a lookup
83
+ * hashes nothing it has not hashed before. */
84
+ function shapeGroupOf(style, levelsKey) {
85
+ const font = style.font;
86
+ let known = shapePrefixes.get(style);
87
+ if (known === undefined || known.font !== font) {
88
+ known = { font, prefix: shapePrefixOf(style), levels: new Map() };
89
+ shapePrefixes.set(style, known);
90
+ }
91
+ // a fragment's levels are one number in the common case, a list only
92
+ // for mixed-direction text; a list is not worth keeping
93
+ if (levelsKey.length > 2) return `${known.prefix}|${levelsKey}`;
94
+ let group = known.levels.get(levelsKey);
95
+ if (group === undefined) {
96
+ group = `${known.prefix}|${levelsKey}`;
97
+ known.levels.set(levelsKey, group);
98
+ }
99
+ return group;
100
+ }
101
+
102
+ function shapePrefixOf(style) {
103
+ const font = style.font;
104
+ // A resolved `font` already carries its coordinates in its key, so the
105
+ // variations and optical-size fragments only earn their keep on the
106
+ // family path — where two points of one axis would otherwise share a
107
+ // shaped run, and the second would be drawn with the first's advances.
108
+ // Features, language and spacing change the glyphs a word shapes to, so
109
+ // they are part of the key on both paths: without them a word shaped once
110
+ // plain answered every later request for it with the plain glyphs, and a
111
+ // `tnum` asked for after that point was silently ignored.
112
+ const shaping = shapingKeyOf(style);
113
+ return font
114
+ ? `${font.key}|${style.size}|${style.weight}|${style.style}|${shaping}`
115
+ : `${style.family}|${style.size}|${style.weight}|${style.style}|${variationsKeyOf(
116
+ style.variations
117
+ )}|${opticalKeyOf(style)}|${shaping}`;
118
+ }
119
+
66
120
  /** Cache-key fragment for the optical size, `''` when the axis is left alone. */
67
121
  function opticalKeyOf(style) {
68
122
  return opticalSizeFor(style) ?? '';
@@ -136,7 +190,11 @@ export default class FontManager {
136
190
  this._matches = new Map(); // family|weight|style -> Font
137
191
  this._fallbacks = new Map(); // family|weight|style -> Map(codepoint -> Font|null)
138
192
  this._registered = []; // { font, family (lowercase), weight, italic }
139
- this._shapeCache = new Map(); // word-level shaping memo (bounded, LRU)
193
+ // word-level shaping memo, in two generations (`_shapeCached`): each a
194
+ // map from a style's key to the words shaped in it
195
+ this._shapeCache = new Map();
196
+ this._shapeCacheBefore = new Map();
197
+ this._shapeCount = 0;
140
198
  }
141
199
 
142
200
  /** the FontSource in effect (explicit, else the process-wide default) */
@@ -355,53 +413,45 @@ export default class FontManager {
355
413
  }
356
414
 
357
415
  /**
358
- * Memoized shaping used by TextLayout and the canvas text path (bounded
359
- * LRU). `levelsKey` is a compact embedding-levels encoding: a single
360
- * number when uniform for the whole fragment (the common case), else
361
- * comma-separated per-char levels.
416
+ * Memoized shaping used by TextLayout and the canvas text path, bounded at
417
+ * two generations of `SHAPE_GENERATION` words. `levelsKey` is a compact
418
+ * embedding-levels encoding: a single number when uniform for the whole
419
+ * fragment (the common case), else comma-separated per-char levels.
362
420
  */
363
421
  _shapeCached(text, style, levelsKey = '0') {
364
- const font = style.font;
365
- // A resolved `font` already carries its coordinates in its key, so the
366
- // variations and optical-size fragments only earn their keep on the
367
- // family path — where two points of one axis would otherwise share a
368
- // shaped run, and the second would be drawn with the first's advances.
369
- // Features, language and spacing change the glyphs a word shapes to, so
370
- // they are part of the key on both paths: without them a word shaped once
371
- // plain answered every later request for it with the plain glyphs, and a
372
- // `tnum` asked for after that point was silently ignored.
373
- const shaping = shapingKeyOf(style);
374
- const key = font
375
- ? `${font.key}|${style.size}|${style.weight}|${style.style}|${shaping}|${levelsKey}|${text}`
376
- : `${style.family}|${style.size}|${style.weight}|${style.style}|${variationsKeyOf(
377
- style.variations
378
- )}|${opticalKeyOf(style)}|${shaping}|${levelsKey}|${text}`;
379
- let shaped = this._shapeCache.get(key);
380
- if (shaped) {
381
- // Map iterates in insertion order: re-inserting a hit moves it to the
382
- // tail, so the eviction sweep below walks least-recently-used first
383
- this._shapeCache.delete(key);
384
- this._shapeCache.set(key, shaped);
385
- return shaped;
422
+ // A hit in this generation is the whole cost of a word a paragraph has
423
+ // shaped before — a relayout at a new width asks for every word again,
424
+ // a hundred thousand of them in a long document — so it is two lookups
425
+ // and nothing built: the style's key is the same string every time
426
+ // (`shapeGroupOf`), and the word is the only thing hashed. What the
427
+ // generation before holds is carried over when asked for; what neither
428
+ // was asked for goes when the generation turns.
429
+ const group = shapeGroupOf(style, levelsKey);
430
+ let words = this._shapeCache.get(group);
431
+ let shaped = words?.get(text);
432
+ if (shaped) return shaped;
433
+ shaped = this._shapeCacheBefore.get(group)?.get(text);
434
+ if (!shaped) {
435
+ let levels;
436
+ if (levelsKey.includes(',')) {
437
+ levels = levelsKey.split(',').map(Number);
438
+ } else {
439
+ levels = new Uint8Array(text.length).fill(Number(levelsKey));
440
+ }
441
+ shaped = shapeText(this, text, style, levels);
386
442
  }
387
- let levels;
388
- if (levelsKey.includes(',')) {
389
- levels = levelsKey.split(',').map(Number);
390
- } else {
391
- levels = new Uint8Array(text.length).fill(Number(levelsKey));
443
+ if (this._shapeCount >= SHAPE_GENERATION) {
444
+ this._shapeCacheBefore = this._shapeCache;
445
+ this._shapeCache = new Map();
446
+ this._shapeCount = 0;
447
+ words = undefined;
392
448
  }
393
- shaped = shapeText(this, text, style, levels);
394
- if (this._shapeCache.size > 4000) {
395
- // drop the stale half in one sweep rather than one entry per insert —
396
- // a live UI's working set sits at the recent end and survives, where
397
- // the old wholesale clear() re-shaped everything on screen
398
- let drop = this._shapeCache.size >> 1;
399
- for (const k of this._shapeCache.keys()) {
400
- if (drop-- <= 0) break;
401
- this._shapeCache.delete(k);
402
- }
449
+ if (!words) {
450
+ words = new Map();
451
+ this._shapeCache.set(group, words);
403
452
  }
404
- this._shapeCache.set(key, shaped);
453
+ words.set(text, shaped);
454
+ this._shapeCount++;
405
455
  return shaped;
406
456
  }
407
457
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ntk",
3
- "version": "8.12.4",
3
+ "version": "8.12.5",
4
4
  "description": "Desktop UI toolkit for X11 with canvas-like 2d and OpenGL rendering",
5
5
  "author": "Andrey Sidorov <sidorares@yandex.ru>",
6
6
  "license": "MIT",