@turntrout/subfont 1.5.0 → 1.6.0
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.
- package/CHANGELOG.md +7 -0
- package/README.md +31 -0
- package/lib/collectTextsByPage.js +109 -337
- package/lib/concurrencyLimit.js +5 -1
- package/lib/fontFeatureHelpers.js +249 -0
- package/lib/fontTracerWorker.js +0 -10
- package/lib/parseCommandLineOptions.js +2 -2
- package/lib/progress.js +101 -0
- package/lib/subfont.js +25 -38
- package/lib/subsetFontWithGlyphs.js +8 -0
- package/lib/subsetFonts.js +60 -91
- package/lib/subsetGeneration.js +14 -1
- package/lib/variationAxes.js +3 -32
- package/lib/wasmQueue.js +4 -1
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -33,3 +33,10 @@ On [TurnTrout.com](https://github.com/alexander-turner/TurnTrout.com) (382 pages
|
|
|
33
33
|
|
|
34
34
|
- Fixed crash on invalid/corrupt font files during instancing.
|
|
35
35
|
- Fixed incorrect axis range computation for variable fonts.
|
|
36
|
+
- Fixed OOM / >1h runtimes on large sites. `font-size` was added to
|
|
37
|
+
`font-tracer`'s `propsToReturn` to derive `opsz`, which bucketed every text
|
|
38
|
+
chunk by size and exploded per-page entry counts 10-50x on sites with many
|
|
39
|
+
distinct sizes (headings, dropcaps, smallcaps). `opsz` now falls back to
|
|
40
|
+
pinning at the font default (the pre-regression behaviour); an explicit
|
|
41
|
+
`font-variation-settings: "opsz" …` still narrows the axis. TurnTrout.com
|
|
42
|
+
returned from 46+ min / runner-OOM to ~33 min.
|
package/README.md
CHANGED
|
@@ -105,6 +105,37 @@ const assetGraph = await subfont(
|
|
|
105
105
|
|
|
106
106
|
Returns the [Assetgraph](https://github.com/assetgraph/assetgraph) instance.
|
|
107
107
|
|
|
108
|
+
### Parameters
|
|
109
|
+
|
|
110
|
+
`subfont(options, console)` — the second argument is an optional logger (anything
|
|
111
|
+
with `log`, `warn`, and `error` methods — e.g. the global `console`). Pass
|
|
112
|
+
`null` together with `silent: true` to suppress all output.
|
|
113
|
+
|
|
114
|
+
The `options` object accepts the following keys:
|
|
115
|
+
|
|
116
|
+
| Option | Type | Default | Description |
|
|
117
|
+
| --------------- | ------------------- | -------- | ------------------------------------------------------------------------------------------------------------ |
|
|
118
|
+
| `inputFiles` | `string[]` | `[]` | HTML entry points (file paths or URLs). At least one is required unless `root` is given. |
|
|
119
|
+
| `root` | `string` | deduced | Path or URL to the web root. Deduced from `inputFiles` if omitted. |
|
|
120
|
+
| `canonicalRoot` | `string` | — | URI root where the site will be deployed (used to rewrite absolute URLs). |
|
|
121
|
+
| `output` | `string` | — | Output directory. Mutually exclusive with `inPlace`. |
|
|
122
|
+
| `inPlace` | `boolean` | `false` | Modify input files in place. |
|
|
123
|
+
| `dryRun` | `boolean` | `false` | Trace and compute subsets but do not write any files. |
|
|
124
|
+
| `recursive` | `boolean` | `false` | Crawl linked pages starting from `inputFiles`. |
|
|
125
|
+
| `dynamic` | `boolean` | `false` | Trace JS-rendered content in headless Chrome (via puppeteer). |
|
|
126
|
+
| `fallbacks` | `boolean` | `true` | Keep the full original font for characters outside the subset. |
|
|
127
|
+
| `fontDisplay` | `string` | `'swap'` | `font-display` CSS value: `auto`, `block`, `swap`, `fallback`, or `optional`. |
|
|
128
|
+
| `text` | `string` | — | Extra characters to include in every subset. |
|
|
129
|
+
| `inlineCss` | `boolean` | `false` | Inline the subset `@font-face` CSS into the HTML document. |
|
|
130
|
+
| `relativeUrls` | `boolean` | `false` | Emit relative URLs instead of root-relative URLs. |
|
|
131
|
+
| `sourceMaps` | `boolean` | `false` | Preserve CSS source maps (slower). |
|
|
132
|
+
| `concurrency` | `number` | auto | Max parallel tracing workers. Capped by available memory (~50 MB per worker). |
|
|
133
|
+
| `chromeFlags` | `string[]` | `[]` | Extra Chrome flags forwarded to puppeteer when `dynamic` is set. |
|
|
134
|
+
| `cache` | `boolean \| string` | `false` | Cache subset results between runs. Pass a path to customize the cache directory; `true` uses the OS tmp dir. |
|
|
135
|
+
| `strict` | `boolean` | `false` | Resolve with a non-zero exit (via the CLI) if any warnings are emitted. |
|
|
136
|
+
| `silent` | `boolean` | `false` | Suppress all log output to `console`. |
|
|
137
|
+
| `debug` | `boolean` | `false` | Emit verbose timing and glyph-detection info. |
|
|
138
|
+
|
|
108
139
|
## License
|
|
109
140
|
|
|
110
141
|
MIT -- Original work by [Peter Muller (Munter)](https://github.com/Munter/subfont)
|
|
@@ -18,22 +18,15 @@ const {
|
|
|
18
18
|
uniqueChars,
|
|
19
19
|
uniqueCharsFromArray,
|
|
20
20
|
} = require('./fontFaceHelpers');
|
|
21
|
+
const {
|
|
22
|
+
createPageProgress,
|
|
23
|
+
logTracedPage,
|
|
24
|
+
makePhaseTracker,
|
|
25
|
+
} = require('./progress');
|
|
21
26
|
|
|
22
27
|
const fontRelevantCssRegex =
|
|
23
28
|
/font-family|font-weight|font-style|font-stretch|font-display|@font-face|font-variation|font-feature/i;
|
|
24
29
|
|
|
25
|
-
// font-tracer defaults omit font-size. We need it for opsz axis mapping.
|
|
26
|
-
const PROPS_TO_RETURN = [
|
|
27
|
-
'font-family',
|
|
28
|
-
'font-style',
|
|
29
|
-
'font-weight',
|
|
30
|
-
'font-variant',
|
|
31
|
-
'font-stretch',
|
|
32
|
-
'font-variation-settings',
|
|
33
|
-
'animation-timing-function',
|
|
34
|
-
'font-size',
|
|
35
|
-
];
|
|
36
|
-
|
|
37
30
|
// The \s before style ensures we don't match data-style or similar.
|
|
38
31
|
const inlineFontStyleRegex =
|
|
39
32
|
/(?:^|\s)style\s*=\s*["'][^"']*\b(?:font-family|font-weight|font-style|font-stretch|font\s*:)/i;
|
|
@@ -47,241 +40,11 @@ const fontFaceTraversalTypes = new Set(['HtmlStyle', 'SvgStyle', 'CssImport']);
|
|
|
47
40
|
// the overhead of worker thread startup exceeds the parallelism benefit).
|
|
48
41
|
const MIN_PAGES_FOR_WORKER_POOL = 4;
|
|
49
42
|
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
'font-variant-ligatures',
|
|
56
|
-
'font-variant-numeric',
|
|
57
|
-
'font-variant-position',
|
|
58
|
-
]);
|
|
59
|
-
|
|
60
|
-
// Map font-variant-* CSS values to their corresponding OpenType feature tags.
|
|
61
|
-
const fontVariantToOTTags = {
|
|
62
|
-
'font-variant-ligatures': {
|
|
63
|
-
'common-ligatures': ['liga', 'clig'],
|
|
64
|
-
'no-common-ligatures': ['liga', 'clig'],
|
|
65
|
-
'discretionary-ligatures': ['dlig'],
|
|
66
|
-
'no-discretionary-ligatures': ['dlig'],
|
|
67
|
-
'historical-ligatures': ['hlig'],
|
|
68
|
-
'no-historical-ligatures': ['hlig'],
|
|
69
|
-
contextual: ['calt'],
|
|
70
|
-
'no-contextual': ['calt'],
|
|
71
|
-
},
|
|
72
|
-
'font-variant-caps': {
|
|
73
|
-
'small-caps': ['smcp'],
|
|
74
|
-
'all-small-caps': ['smcp', 'c2sc'],
|
|
75
|
-
'petite-caps': ['pcap'],
|
|
76
|
-
'all-petite-caps': ['pcap', 'c2pc'],
|
|
77
|
-
unicase: ['unic'],
|
|
78
|
-
'titling-caps': ['titl'],
|
|
79
|
-
},
|
|
80
|
-
'font-variant-numeric': {
|
|
81
|
-
'lining-nums': ['lnum'],
|
|
82
|
-
'oldstyle-nums': ['onum'],
|
|
83
|
-
'proportional-nums': ['pnum'],
|
|
84
|
-
'tabular-nums': ['tnum'],
|
|
85
|
-
'diagonal-fractions': ['frac'],
|
|
86
|
-
'stacked-fractions': ['afrc'],
|
|
87
|
-
ordinal: ['ordn'],
|
|
88
|
-
'slashed-zero': ['zero'],
|
|
89
|
-
},
|
|
90
|
-
'font-variant-position': {
|
|
91
|
-
sub: ['subs'],
|
|
92
|
-
super: ['sups'],
|
|
93
|
-
},
|
|
94
|
-
'font-variant-east-asian': {
|
|
95
|
-
jis78: ['jp78'],
|
|
96
|
-
jis83: ['jp83'],
|
|
97
|
-
jis90: ['jp90'],
|
|
98
|
-
jis04: ['jp04'],
|
|
99
|
-
simplified: ['smpl'],
|
|
100
|
-
traditional: ['trad'],
|
|
101
|
-
'proportional-width': ['pwid'],
|
|
102
|
-
'full-width': ['fwid'],
|
|
103
|
-
ruby: ['ruby'],
|
|
104
|
-
},
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
// Extract OpenType feature tags referenced by a CSS declaration.
|
|
108
|
-
function extractFeatureTagsFromDecl(prop, value) {
|
|
109
|
-
const tags = new Set();
|
|
110
|
-
const propLower = prop.toLowerCase();
|
|
111
|
-
|
|
112
|
-
if (propLower === 'font-feature-settings') {
|
|
113
|
-
// Parse quoted 4-letter tags: "liga" 1, 'dlig', etc.
|
|
114
|
-
const re = /["']([a-zA-Z0-9]{4})["']/g;
|
|
115
|
-
let m;
|
|
116
|
-
while ((m = re.exec(value)) !== null) {
|
|
117
|
-
tags.add(m[1]);
|
|
118
|
-
}
|
|
119
|
-
return tags;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
if (propLower === 'font-variant-alternates') {
|
|
123
|
-
const v = value.toLowerCase();
|
|
124
|
-
if (v.includes('historical-forms')) tags.add('hist');
|
|
125
|
-
if (/stylistic\s*\(/.test(v)) tags.add('salt');
|
|
126
|
-
if (/swash\s*\(/.test(v)) tags.add('swsh');
|
|
127
|
-
if (/ornaments\s*\(/.test(v)) tags.add('ornm');
|
|
128
|
-
if (/annotation\s*\(/.test(v)) tags.add('nalt');
|
|
129
|
-
if (/styleset\s*\(/.test(v)) {
|
|
130
|
-
for (let i = 1; i <= 20; i++) {
|
|
131
|
-
tags.add(`ss${String(i).padStart(2, '0')}`);
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
if (/character-variant\s*\(/.test(v)) {
|
|
135
|
-
for (let i = 1; i <= 99; i++) {
|
|
136
|
-
tags.add(`cv${String(i).padStart(2, '0')}`);
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
return tags;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
const mapping = fontVariantToOTTags[propLower];
|
|
143
|
-
if (mapping) {
|
|
144
|
-
// Split into tokens for exact keyword matching — substring matching
|
|
145
|
-
// would falsely trigger e.g. "sub" inside "super".
|
|
146
|
-
const tokens = new Set(value.toLowerCase().split(/\s+/));
|
|
147
|
-
for (const [keyword, otTags] of Object.entries(mapping)) {
|
|
148
|
-
if (tokens.has(keyword)) {
|
|
149
|
-
for (const t of otTags) tags.add(t);
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
return tags;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// Collect feature tags from all feature-related declarations in a CSS rule.
|
|
157
|
-
function ruleFeatureTags(rule) {
|
|
158
|
-
const tags = new Set();
|
|
159
|
-
let hasFeatureDecl = false;
|
|
160
|
-
for (const node of rule.nodes) {
|
|
161
|
-
if (
|
|
162
|
-
node.type === 'decl' &&
|
|
163
|
-
featureSettingsProps.has(node.prop.toLowerCase())
|
|
164
|
-
) {
|
|
165
|
-
hasFeatureDecl = true;
|
|
166
|
-
for (const t of extractFeatureTagsFromDecl(node.prop, node.value)) {
|
|
167
|
-
tags.add(t);
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
return hasFeatureDecl ? tags : null;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
function ruleFontFamily(rule) {
|
|
175
|
-
for (let i = rule.nodes.length - 1; i >= 0; i--) {
|
|
176
|
-
const node = rule.nodes[i];
|
|
177
|
-
if (node.type === 'decl' && node.prop.toLowerCase() === 'font-family') {
|
|
178
|
-
return node.value;
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
return null;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// Add all items from `tags` into the Set stored at `key` in `map`,
|
|
185
|
-
// creating the Set if it doesn't exist yet.
|
|
186
|
-
function addTagsToMapEntry(map, key, tags) {
|
|
187
|
-
let s = map.get(key);
|
|
188
|
-
if (!s) {
|
|
189
|
-
s = new Set();
|
|
190
|
-
map.set(key, s);
|
|
191
|
-
}
|
|
192
|
-
for (const t of tags) s.add(t);
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// Record the OT tags from a single CSS rule into featureTagsByFamily,
|
|
196
|
-
// keyed by font-family (or '*' when no font-family is specified).
|
|
197
|
-
function recordRuleFeatureTags(rule, featureTagsByFamily) {
|
|
198
|
-
const tags = ruleFeatureTags(rule);
|
|
199
|
-
if (!tags) return null;
|
|
200
|
-
|
|
201
|
-
const fontFamily = ruleFontFamily(rule);
|
|
202
|
-
if (!fontFamily) {
|
|
203
|
-
if (featureTagsByFamily) addTagsToMapEntry(featureTagsByFamily, '*', tags);
|
|
204
|
-
return true; // signals "all families"
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
const families = cssFontParser.parseFontFamily(fontFamily);
|
|
208
|
-
if (featureTagsByFamily) {
|
|
209
|
-
for (const family of families) {
|
|
210
|
-
addTagsToMapEntry(featureTagsByFamily, family.toLowerCase(), tags);
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
return families;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// Determine which font-families use font-feature-settings or font-variant-*.
|
|
217
|
-
// Returns null (none detected), a Set of lowercase family names, or true (all).
|
|
218
|
-
// Also populates featureTagsByFamily with the OT tags per family (lowercase).
|
|
219
|
-
function findFontFamiliesWithFeatureSettings(
|
|
220
|
-
stylesheetsWithPredicates,
|
|
221
|
-
featureTagsByFamily
|
|
222
|
-
) {
|
|
223
|
-
let result = null;
|
|
224
|
-
for (const { asset } of stylesheetsWithPredicates) {
|
|
225
|
-
if (!asset || !asset.parseTree) continue;
|
|
226
|
-
asset.parseTree.walkRules((rule) => {
|
|
227
|
-
if (result === true && !featureTagsByFamily) return;
|
|
228
|
-
|
|
229
|
-
const recorded = recordRuleFeatureTags(rule, featureTagsByFamily);
|
|
230
|
-
if (!recorded) return;
|
|
231
|
-
|
|
232
|
-
if (recorded === true) {
|
|
233
|
-
result = true;
|
|
234
|
-
} else if (result !== true) {
|
|
235
|
-
if (!result) result = new Set();
|
|
236
|
-
for (const family of recorded) {
|
|
237
|
-
result.add(family.toLowerCase());
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
});
|
|
241
|
-
if (result === true && !featureTagsByFamily) break;
|
|
242
|
-
}
|
|
243
|
-
return result;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
// Determine whether a template's font families use feature settings, and
|
|
247
|
-
// collect the corresponding OT feature tags from featureTagsByFamily.
|
|
248
|
-
function resolveFeatureSettings(
|
|
249
|
-
fontFamilies,
|
|
250
|
-
fontFamiliesWithFeatureSettings,
|
|
251
|
-
featureTagsByFamily
|
|
252
|
-
) {
|
|
253
|
-
let hasFontFeatureSettings = false;
|
|
254
|
-
if (fontFamiliesWithFeatureSettings === true) {
|
|
255
|
-
hasFontFeatureSettings = true;
|
|
256
|
-
} else if (fontFamiliesWithFeatureSettings instanceof Set) {
|
|
257
|
-
for (const f of fontFamilies) {
|
|
258
|
-
if (fontFamiliesWithFeatureSettings.has(f.toLowerCase())) {
|
|
259
|
-
hasFontFeatureSettings = true;
|
|
260
|
-
break;
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
let fontFeatureTags;
|
|
266
|
-
if (hasFontFeatureSettings && featureTagsByFamily) {
|
|
267
|
-
const tags = new Set();
|
|
268
|
-
const globalTags = featureTagsByFamily.get('*');
|
|
269
|
-
if (globalTags) {
|
|
270
|
-
for (const t of globalTags) tags.add(t);
|
|
271
|
-
}
|
|
272
|
-
for (const f of fontFamilies) {
|
|
273
|
-
const familyTags = featureTagsByFamily.get(f.toLowerCase());
|
|
274
|
-
if (familyTags) {
|
|
275
|
-
for (const t of familyTags) tags.add(t);
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
if (tags.size > 0) {
|
|
279
|
-
fontFeatureTags = [...tags];
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
return { hasFontFeatureSettings, fontFeatureTags };
|
|
284
|
-
}
|
|
43
|
+
const {
|
|
44
|
+
extractFeatureTagsFromDecl,
|
|
45
|
+
findFontFamiliesWithFeatureSettings,
|
|
46
|
+
resolveFeatureSettings,
|
|
47
|
+
} = require('./fontFeatureHelpers');
|
|
285
48
|
|
|
286
49
|
const allInitialValues = require('./initialValueByProp');
|
|
287
50
|
const initialValueByProp = {
|
|
@@ -398,7 +161,6 @@ function computeSnappedGlobalEntries(declarations, globalTextByProps) {
|
|
|
398
161
|
textAndProps,
|
|
399
162
|
...snapped,
|
|
400
163
|
fontVariationSettings: textAndProps.props['font-variation-settings'],
|
|
401
|
-
fontSize: textAndProps.props['font-size'],
|
|
402
164
|
});
|
|
403
165
|
}
|
|
404
166
|
}
|
|
@@ -505,9 +267,6 @@ function getOrComputeGlobalFontUsages(
|
|
|
505
267
|
.map((e) => e.fontVariationSettings)
|
|
506
268
|
.filter((fvs) => fvs && fvs.toLowerCase() !== 'normal')
|
|
507
269
|
);
|
|
508
|
-
const fontSizes = new Set(
|
|
509
|
-
fontEntries.map((e) => e.fontSize).filter((fs) => fs !== undefined)
|
|
510
|
-
);
|
|
511
270
|
// Use first entry's relations for size computation, or extra's if no entries
|
|
512
271
|
const fontRelations =
|
|
513
272
|
fontEntries.length > 0
|
|
@@ -543,7 +302,6 @@ function getOrComputeGlobalFontUsages(
|
|
|
543
302
|
fontStretches,
|
|
544
303
|
fontWeights,
|
|
545
304
|
fontVariationSettings,
|
|
546
|
-
fontSizes,
|
|
547
305
|
});
|
|
548
306
|
}
|
|
549
307
|
|
|
@@ -556,71 +314,100 @@ function getOrComputeGlobalFontUsages(
|
|
|
556
314
|
// cyclomatic complexity of collectTextsByPage.
|
|
557
315
|
async function tracePages(
|
|
558
316
|
pagesNeedingFullTrace,
|
|
559
|
-
{
|
|
317
|
+
{
|
|
318
|
+
headlessBrowser,
|
|
319
|
+
concurrency,
|
|
320
|
+
console,
|
|
321
|
+
memoizedGetCssRulesByProperty,
|
|
322
|
+
debug = false,
|
|
323
|
+
}
|
|
560
324
|
) {
|
|
325
|
+
const totalPages = pagesNeedingFullTrace.length;
|
|
326
|
+
if (totalPages === 0) return;
|
|
327
|
+
|
|
561
328
|
const useWorkerPool =
|
|
562
|
-
!headlessBrowser &&
|
|
563
|
-
|
|
329
|
+
!headlessBrowser && totalPages >= MIN_PAGES_FOR_WORKER_POOL;
|
|
330
|
+
|
|
331
|
+
const progress = createPageProgress({
|
|
332
|
+
total: totalPages,
|
|
333
|
+
console,
|
|
334
|
+
label: 'Tracing fonts',
|
|
335
|
+
});
|
|
564
336
|
|
|
565
337
|
if (useWorkerPool) {
|
|
566
338
|
const maxWorkers =
|
|
567
339
|
concurrency > 0 ? concurrency : Math.min(os.cpus().length, 8);
|
|
568
|
-
const numWorkers = Math.min(maxWorkers,
|
|
340
|
+
const numWorkers = Math.min(maxWorkers, totalPages);
|
|
569
341
|
const pool = new FontTracerPool(numWorkers);
|
|
570
342
|
await pool.init();
|
|
571
343
|
|
|
572
344
|
try {
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
pd.
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
if (console) {
|
|
584
|
-
console.warn(
|
|
585
|
-
`Worker fontTracer failed for ${pd.htmlOrSvgAsset.url}, falling back to main thread: ${err.message}`
|
|
345
|
+
progress.banner(
|
|
346
|
+
` Tracing fonts across ${totalPages} pages using ${numWorkers} worker${numWorkers === 1 ? '' : 's'}...`
|
|
347
|
+
);
|
|
348
|
+
await Promise.all(
|
|
349
|
+
pagesNeedingFullTrace.map(async (pd) => {
|
|
350
|
+
const pageStart = debug ? Date.now() : 0;
|
|
351
|
+
try {
|
|
352
|
+
pd.textByProps = await pool.trace(
|
|
353
|
+
pd.htmlOrSvgAsset.text || '',
|
|
354
|
+
pd.stylesheetsWithPredicates
|
|
586
355
|
);
|
|
356
|
+
} catch (err) {
|
|
357
|
+
if (console) {
|
|
358
|
+
console.warn(
|
|
359
|
+
`Worker fontTracer failed for ${pd.htmlOrSvgAsset.url}, falling back to main thread: ${err.message}`
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
pd.textByProps = fontTracer(pd.htmlOrSvgAsset.parseTree, {
|
|
363
|
+
stylesheetsWithPredicates: pd.stylesheetsWithPredicates,
|
|
364
|
+
getCssRulesByProperty: memoizedGetCssRulesByProperty,
|
|
365
|
+
asset: pd.htmlOrSvgAsset,
|
|
366
|
+
});
|
|
587
367
|
}
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
await Promise.all(tracePromises);
|
|
368
|
+
const idx = progress.tick();
|
|
369
|
+
logTracedPage(
|
|
370
|
+
console,
|
|
371
|
+
debug,
|
|
372
|
+
idx,
|
|
373
|
+
totalPages,
|
|
374
|
+
pd.htmlOrSvgAsset,
|
|
375
|
+
pageStart
|
|
376
|
+
);
|
|
377
|
+
})
|
|
378
|
+
);
|
|
379
|
+
progress.done();
|
|
601
380
|
} finally {
|
|
602
381
|
await pool.destroy();
|
|
603
382
|
}
|
|
604
|
-
} else
|
|
605
|
-
|
|
606
|
-
|
|
383
|
+
} else {
|
|
384
|
+
progress.banner(
|
|
385
|
+
` Tracing fonts across ${totalPages} page${totalPages === 1 ? '' : 's'} (single-threaded${headlessBrowser ? ' + headless browser' : ''})...`
|
|
386
|
+
);
|
|
607
387
|
for (let pi = 0; pi < totalPages; pi++) {
|
|
608
388
|
const pd = pagesNeedingFullTrace[pi];
|
|
389
|
+
const pageStart = debug ? Date.now() : 0;
|
|
609
390
|
pd.textByProps = fontTracer(pd.htmlOrSvgAsset.parseTree, {
|
|
610
391
|
stylesheetsWithPredicates: pd.stylesheetsWithPredicates,
|
|
611
392
|
getCssRulesByProperty: memoizedGetCssRulesByProperty,
|
|
612
393
|
asset: pd.htmlOrSvgAsset,
|
|
613
|
-
propsToReturn: PROPS_TO_RETURN,
|
|
614
394
|
});
|
|
615
395
|
if (headlessBrowser) {
|
|
616
396
|
pd.textByProps.push(
|
|
617
397
|
...(await headlessBrowser.tracePage(pd.htmlOrSvgAsset))
|
|
618
398
|
);
|
|
619
399
|
}
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
400
|
+
const idx = progress.tick();
|
|
401
|
+
logTracedPage(
|
|
402
|
+
console,
|
|
403
|
+
debug,
|
|
404
|
+
idx,
|
|
405
|
+
totalPages,
|
|
406
|
+
pd.htmlOrSvgAsset,
|
|
407
|
+
pageStart
|
|
408
|
+
);
|
|
623
409
|
}
|
|
410
|
+
progress.done();
|
|
624
411
|
}
|
|
625
412
|
}
|
|
626
413
|
|
|
@@ -629,11 +416,11 @@ async function tracePages(
|
|
|
629
416
|
// props and only extract visible text content.
|
|
630
417
|
function processFastPathPages(
|
|
631
418
|
fastPathPages,
|
|
632
|
-
{ memoizedGetCssRulesByProperty,
|
|
419
|
+
{ memoizedGetCssRulesByProperty, subTimings, trackPhase }
|
|
633
420
|
) {
|
|
634
421
|
if (fastPathPages.length === 0) return;
|
|
635
422
|
|
|
636
|
-
const
|
|
423
|
+
const fastPathPhase = trackPhase('Fast-path extraction');
|
|
637
424
|
|
|
638
425
|
const repDataCache = new Map();
|
|
639
426
|
function getRepData(representativePd) {
|
|
@@ -682,7 +469,6 @@ function processFastPathPages(
|
|
|
682
469
|
stylesheetsWithPredicates: pd.stylesheetsWithPredicates,
|
|
683
470
|
getCssRulesByProperty: memoizedGetCssRulesByProperty,
|
|
684
471
|
asset: pd.htmlOrSvgAsset,
|
|
685
|
-
propsToReturn: PROPS_TO_RETURN,
|
|
686
472
|
});
|
|
687
473
|
continue;
|
|
688
474
|
}
|
|
@@ -742,11 +528,9 @@ function processFastPathPages(
|
|
|
742
528
|
});
|
|
743
529
|
}
|
|
744
530
|
}
|
|
745
|
-
subTimings['Fast-path extraction'] =
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
`[subfont timing] Fast-path text extraction (${fastPathPages.length} pages, ${fastPathFallbacks} fell back to full trace): ${subTimings['Fast-path extraction']}ms`
|
|
749
|
-
);
|
|
531
|
+
subTimings['Fast-path extraction'] = fastPathPhase.end(
|
|
532
|
+
`${fastPathPages.length} pages, ${fastPathFallbacks} fell back to full trace`
|
|
533
|
+
);
|
|
750
534
|
}
|
|
751
535
|
|
|
752
536
|
async function collectTextsByPage(
|
|
@@ -933,12 +717,11 @@ async function collectTextsByPage(
|
|
|
933
717
|
const globalTextByProps = [];
|
|
934
718
|
const subTimings = {};
|
|
935
719
|
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
const timingStart = Date.now();
|
|
720
|
+
const trackPhase = makePhaseTracker(console, debug);
|
|
721
|
+
const overallPhase = trackPhase('collectTextsByPage');
|
|
939
722
|
|
|
940
723
|
// Pre-compute stylesheet results for all pages
|
|
941
|
-
const
|
|
724
|
+
const stylesheetPrecompute = trackPhase('Stylesheet precompute');
|
|
942
725
|
const pageData = [];
|
|
943
726
|
for (const htmlOrSvgAsset of htmlOrSvgAssets) {
|
|
944
727
|
const {
|
|
@@ -967,10 +750,9 @@ async function collectTextsByPage(
|
|
|
967
750
|
});
|
|
968
751
|
}
|
|
969
752
|
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
);
|
|
753
|
+
subTimings['Stylesheet precompute'] = stylesheetPrecompute.end(
|
|
754
|
+
`${pageData.length} pages with fonts`
|
|
755
|
+
);
|
|
974
756
|
|
|
975
757
|
// Group pages by stylesheet cache key — pages sharing the same CSS
|
|
976
758
|
// configuration produce identical font-tracer props, only text differs.
|
|
@@ -1000,34 +782,38 @@ async function collectTextsByPage(
|
|
|
1000
782
|
}
|
|
1001
783
|
}
|
|
1002
784
|
|
|
1003
|
-
|
|
785
|
+
// Always surface the per-page work breakdown so users can tell at a
|
|
786
|
+
// glance how much of the run is actual tracing vs cheap CSS-group
|
|
787
|
+
// reuse. The threshold matches createPageProgress's minTotal so it
|
|
788
|
+
// only appears on non-trivial runs.
|
|
789
|
+
if (console && pageData.length >= 5) {
|
|
1004
790
|
console.log(
|
|
1005
|
-
`
|
|
791
|
+
` ${pageData.length} pages with fonts: ${pagesNeedingFullTrace.length} to trace, ${fastPathPages.length} via cached CSS group (${pagesByStylesheetKey.size} unique groups)`
|
|
1006
792
|
);
|
|
793
|
+
}
|
|
1007
794
|
|
|
1008
795
|
const tracingStart = Date.now();
|
|
796
|
+
const fullTracing = trackPhase(
|
|
797
|
+
`Full tracing (${pagesNeedingFullTrace.length} pages)`
|
|
798
|
+
);
|
|
1009
799
|
try {
|
|
1010
800
|
await tracePages(pagesNeedingFullTrace, {
|
|
1011
801
|
headlessBrowser,
|
|
1012
802
|
concurrency,
|
|
1013
803
|
console,
|
|
1014
804
|
memoizedGetCssRulesByProperty,
|
|
805
|
+
debug,
|
|
1015
806
|
});
|
|
1016
807
|
|
|
1017
|
-
subTimings['Full tracing'] =
|
|
1018
|
-
if (debug && console)
|
|
1019
|
-
console.log(
|
|
1020
|
-
`[subfont timing] Full tracing (${pagesNeedingFullTrace.length} pages): ${subTimings['Full tracing']}ms`
|
|
1021
|
-
);
|
|
808
|
+
subTimings['Full tracing'] = fullTracing.end();
|
|
1022
809
|
|
|
1023
810
|
processFastPathPages(fastPathPages, {
|
|
1024
811
|
memoizedGetCssRulesByProperty,
|
|
1025
|
-
console,
|
|
1026
|
-
debug,
|
|
1027
812
|
subTimings,
|
|
813
|
+
trackPhase,
|
|
1028
814
|
});
|
|
1029
815
|
|
|
1030
|
-
const
|
|
816
|
+
const assemblePhase = trackPhase('Result assembly');
|
|
1031
817
|
for (const pd of pageData) {
|
|
1032
818
|
for (const textByPropsEntry of pd.textByProps) {
|
|
1033
819
|
textByPropsEntry.htmlOrSvgAsset = pd.htmlOrSvgAsset;
|
|
@@ -1044,12 +830,8 @@ async function collectTextsByPage(
|
|
|
1044
830
|
featureTagsByFamily: pd.featureTagsByFamily,
|
|
1045
831
|
});
|
|
1046
832
|
}
|
|
1047
|
-
|
|
1048
|
-
subTimings['Result assembly'] = Date.now() - assembleStart;
|
|
833
|
+
subTimings['Result assembly'] = assemblePhase.end();
|
|
1049
834
|
if (debug && console) {
|
|
1050
|
-
console.log(
|
|
1051
|
-
`[subfont timing] Result assembly: ${subTimings['Result assembly']}ms`
|
|
1052
|
-
);
|
|
1053
835
|
console.log(
|
|
1054
836
|
`[subfont timing] Total tracing+extraction+assembly: ${
|
|
1055
837
|
Date.now() - tracingStart
|
|
@@ -1062,12 +844,12 @@ async function collectTextsByPage(
|
|
|
1062
844
|
}
|
|
1063
845
|
}
|
|
1064
846
|
|
|
1065
|
-
const
|
|
847
|
+
const postProcessPhase = trackPhase('Post-processing total');
|
|
1066
848
|
|
|
1067
849
|
// Consolidated cache for per-declarations-key data.
|
|
1068
850
|
const declCache = new Map();
|
|
1069
851
|
|
|
1070
|
-
const
|
|
852
|
+
const perPageLoopPhase = trackPhase('Per-page loop');
|
|
1071
853
|
let snappingTime = 0;
|
|
1072
854
|
let globalUsageTime = 0;
|
|
1073
855
|
|
|
@@ -1162,7 +944,6 @@ async function collectTextsByPage(
|
|
|
1162
944
|
fontStretches: template.fontStretches,
|
|
1163
945
|
fontWeights: template.fontWeights,
|
|
1164
946
|
fontVariationSettings: template.fontVariationSettings,
|
|
1165
|
-
fontSizes: template.fontSizes,
|
|
1166
947
|
preload: preloadFontUrls.has(template.fontUrl),
|
|
1167
948
|
hasFontFeatureSettings,
|
|
1168
949
|
fontFeatureTags,
|
|
@@ -1172,20 +953,11 @@ async function collectTextsByPage(
|
|
|
1172
953
|
cloningTime += Date.now() - cloneStart;
|
|
1173
954
|
}
|
|
1174
955
|
|
|
1175
|
-
subTimings['Per-page loop'] =
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
);
|
|
1181
|
-
if (debug && console)
|
|
1182
|
-
console.log(
|
|
1183
|
-
`[subfont timing] Post-processing total: ${subTimings['Post-processing total']}ms`
|
|
1184
|
-
);
|
|
1185
|
-
if (debug && console)
|
|
1186
|
-
console.log(
|
|
1187
|
-
`[subfont timing] collectTextsByPage total: ${Date.now() - timingStart}ms`
|
|
1188
|
-
);
|
|
956
|
+
subTimings['Per-page loop'] = perPageLoopPhase.end(
|
|
957
|
+
`snapping: ${snappingTime}ms, globalUsage: ${globalUsageTime}ms, cloning: ${cloningTime}ms`
|
|
958
|
+
);
|
|
959
|
+
subTimings['Post-processing total'] = postProcessPhase.end();
|
|
960
|
+
overallPhase.end();
|
|
1189
961
|
|
|
1190
962
|
for (const fontFaceDeclarations of fontFaceDeclarationsByHtmlOrSvgAsset.values()) {
|
|
1191
963
|
for (const fontFaceDeclaration of fontFaceDeclarations) {
|
package/lib/concurrencyLimit.js
CHANGED
|
@@ -4,7 +4,11 @@ const os = require('os');
|
|
|
4
4
|
const WORKER_MEMORY_BYTES = 50 * 1024 * 1024; // 50 MB
|
|
5
5
|
|
|
6
6
|
function getMaxConcurrency() {
|
|
7
|
-
|
|
7
|
+
const byMemory = Math.floor(os.freemem() / WORKER_MEMORY_BYTES);
|
|
8
|
+
// Font tracing is CPU-bound (not I/O), so match the pool size to the
|
|
9
|
+
// core count directly — no multiplier.
|
|
10
|
+
const byCpu = os.cpus().length;
|
|
11
|
+
return Math.max(1, Math.min(byMemory, byCpu));
|
|
8
12
|
}
|
|
9
13
|
|
|
10
14
|
module.exports = { WORKER_MEMORY_BYTES, getMaxConcurrency };
|