Optimize the garbage collector to produce smaller files

This commit is contained in:
be5invis 2023-08-25 03:11:15 -07:00
parent a3836f8144
commit 80700d9dc8
8 changed files with 322 additions and 70 deletions

View file

@ -3,7 +3,7 @@ import zlib from "zlib";
import { encode, decode } from "@msgpack/msgpack"; import { encode, decode } from "@msgpack/msgpack";
const Edition = 28; const Edition = 29;
const MAX_AGE = 16; const MAX_AGE = 16;
class GfEntry { class GfEntry {
constructor(age, value) { constructor(age, value) {

View file

@ -1,22 +1,28 @@
import * as Geometry from "../../support/geometry/index.mjs";
import { Transform } from "../../support/geometry/transform.mjs";
import { Radical, VS01 } from "../../support/gr.mjs"; import { Radical, VS01 } from "../../support/gr.mjs";
export function gcFont(glyphStore, excludedChars, otl, cfg) { export function gcFont(glyphStore, excludedChars, otl) {
markSweepOtlLookups(otl.GSUB); const daGsub = markSweepOtlLookups(otl.GSUB);
markSweepOtlLookups(otl.GPOS); markSweepOtlLookups(otl.GPOS);
const sink = markGlyphs(glyphStore, excludedChars, otl, cfg); const sink = markGlyphs(glyphStore, excludedChars, otl, daGsub);
return sweepGlyphs(glyphStore, sink); return sweepGlyphs(glyphStore, sink);
} }
///////////////////////////////////////////////////////////////////////////////////////////////////
function markSweepOtlLookups(table) { function markSweepOtlLookups(table) {
if (!table || !table.features || !table.lookups) return; if (!table || !table.features || !table.lookups) return;
const accessibleLookupsIds = new Set(); const accessibleLookupsIds = new Set();
markLookups(table, accessibleLookupsIds); const directAccessibleLookupsIds = new Set();
markLookups(table, accessibleLookupsIds, directAccessibleLookupsIds);
sweepLookups(table, accessibleLookupsIds); sweepLookups(table, accessibleLookupsIds);
sweepFeatures(table, accessibleLookupsIds); sweepFeatures(table, accessibleLookupsIds);
return directAccessibleLookupsIds;
} }
function markLookups(table, sink) { function markLookups(table, sink, sinkDirect) {
if (!table || !table.features) return; if (!table || !table.features) return;
markLookupsStart(table, sink); markLookupsStart(table, sink, sinkDirect);
let loop = 0, let loop = 0,
lookupSetChanged = false; lookupSetChanged = false;
do { do {
@ -36,11 +42,14 @@ function markLookups(table, sink) {
lookupSetChanged = sizeBefore !== sink.size; lookupSetChanged = sizeBefore !== sink.size;
} while (loop < 0xff && lookupSetChanged); } while (loop < 0xff && lookupSetChanged);
} }
function markLookupsStart(table, sink) { function markLookupsStart(table, sink, sinkDirect) {
for (let f in table.features) { for (let f in table.features) {
const feature = table.features[f]; const feature = table.features[f];
if (!feature) continue; if (!feature) continue;
for (const l of feature) sink.add(l); for (const l of feature) {
sink.add(l);
sinkDirect.add(l);
}
} }
} }
function sweepLookups(table, accessibleLookupsIds) { function sweepLookups(table, accessibleLookupsIds) {
@ -64,89 +73,128 @@ function sweepFeatures(table, accessibleLookupsIds) {
table.features = features1; table.features = features1;
} }
function markGlyphs(glyphStore, excludedChars, otl, cfg) { ///////////////////////////////////////////////////////////////////////////////////////////////////
const sink = markGlyphsInitial(glyphStore, excludedChars);
while (markGlyphsStep(glyphStore, sink, otl, cfg)); function markGlyphs(glyphStore, excludedChars, otl, daGsub) {
return sink; const markedGlyphs = markGlyphsInitial(glyphStore, excludedChars);
while (markGlyphsGr(glyphStore, markedGlyphs, otl));
if (otl.GSUB) markGlyphsByGsub(otl.GSUB, markedGlyphs, daGsub);
while (markGlyphsGr(glyphStore, markedGlyphs, otl));
analyzeReferenceGraph(glyphStore, markedGlyphs);
return markedGlyphs;
} }
function markSingleGlyph(markedGlyphs, gName, d) {
let existing = markedGlyphs.get(gName);
if (!existing || d < existing) markedGlyphs.set(gName, d);
}
function markGlyphsInitial(glyphStore, excludedChars) { function markGlyphsInitial(glyphStore, excludedChars) {
let sink = new Set(); let markedGlyphs = new Map();
for (const [gName, g] of glyphStore.namedEntries()) { for (const [gName, g] of glyphStore.namedEntries()) {
if (!g) continue; if (!g) continue;
if (g.glyphRank > 0) sink.add(gName); if (g.glyphRank > 0) markSingleGlyph(markedGlyphs, gName, 1);
if (Radical.get(g)) sink.add(gName); if (Radical.get(g)) markSingleGlyph(markedGlyphs, gName, 1);
const unicodeSet = glyphStore.queryUnicodeOf(g); const unicodeSet = glyphStore.queryUnicodeOf(g);
if (unicodeSet) { if (unicodeSet) {
for (const u of unicodeSet) { for (const u of unicodeSet) {
if (!excludedChars.has(u)) sink.add(gName); if (excludedChars.has(u)) continue;
let d = Math.max(1, Math.min(u, 0xffff) >> 4);
markSingleGlyph(markedGlyphs, gName, d);
} }
} }
} }
return sink;
return markedGlyphs;
} }
function markGlyphsStep(glyphStore, sink, otl, cfg) {
const glyphCount = sink.size; function markGlyphsGr(glyphStore, markedGlyphs, otl) {
const glyphCount = markedGlyphs.size;
for (const g of glyphStore.glyphs()) { for (const g of glyphStore.glyphs()) {
markLinkedGlyph(sink, g, VS01); markLinkedGlyph(markedGlyphs, g, VS01);
} }
if (otl.GSUB) { const glyphCount1 = markedGlyphs.size;
for (const l in otl.GSUB.lookups) {
const lookup = otl.GSUB.lookups[l];
if (!lookup) continue;
markGlyphsLookupImpl(sink, lookup, cfg);
}
}
const glyphCount1 = sink.size;
return glyphCount1 > glyphCount; return glyphCount1 > glyphCount;
} }
function markLinkedGlyph(sink, g, gr) { function markLinkedGlyph(markedGlyphs, g, gr) {
const linked = gr.get(g); const linked = gr.get(g);
if (linked) sink.add(linked); const d = markedGlyphs.get(g);
if (d && linked) markSingleGlyph(markedGlyphs, linked, d + 0x1000);
} }
function markGlyphsLookupImpl(sink, lookup, cfg) {
function markGlyphsByGsub(gsub, markedGlyphs, daGsub) {
for (const lid of gsub.lookupOrder) {
if (!daGsub.has(lid)) continue;
markGlyphsByLookup(gsub, lid, markedGlyphs);
}
}
function markGlyphsByLookup(gsub, lid, markedGlyphs) {
const lookup = gsub.lookups[lid];
if (!lookup) return;
switch (lookup.type) { switch (lookup.type) {
case "gsub_single": case "gsub_single":
return markGlyphsGsubSingle(sink, lookup, cfg); return markGlyphsGsubSingle(markedGlyphs, lookup);
case "gsub_multiple": case "gsub_multiple":
return markGlyphsGsubMultiple(sink, lookup, cfg); return markGlyphsGsubMultiple(markedGlyphs, lookup);
case "gsub_alternate": case "gsub_alternate":
return markGlyphsGsubAlternate(sink, lookup, cfg); return markGlyphsGsubAlternate(markedGlyphs, lookup);
case "gsub_ligature": case "gsub_ligature":
return markGlyphsGsubLigature(sink, lookup, cfg); return markGlyphsGsubLigature(markedGlyphs, lookup);
case "gsub_chaining": case "gsub_chaining": {
rules: for (const rule of lookup.rules) {
// Check whether all match coverages has at least one glyph in the sink
for (const m of rule.match) {
let atLeastOneMatch = false;
for (const matchGlyph of m)
if (markedGlyphs.has(matchGlyph)) atLeastOneMatch = true;
if (!atLeastOneMatch) continue rules;
}
// If so traverse through the lookup applications
for (const app of rule.apply) markGlyphsByLookup(gsub, app.lookup, markedGlyphs);
}
break; break;
}
case "gsub_reverse": case "gsub_reverse":
return markGlyphsGsubReverse(sink, lookup, cfg); return markGlyphsGsubReverse(markedGlyphs, lookup);
} }
} }
function markGlyphsGsubSingle(sink, lookup, cfg) {
function markGlyphsGsubSingle(markedGlyphs, lookup) {
const st = lookup.substitutions; const st = lookup.substitutions;
for (const k in st) if (sink.has(k) && st[k]) sink.add(st[k]); for (const k in st) {
} const d = markedGlyphs.get(k);
function markGlyphsGsubMultiple(sink, lookup, cfg) { if (d && st[k]) markSingleGlyph(markedGlyphs, st[k], d + 0x1000);
const st = lookup.substitutions;
for (const k in st) if (sink.has(k) && st[k]) for (const g of st[k]) sink.add(g);
}
function markGlyphsGsubAlternate(sink, lookup, cfg) {
const st = lookup.substitutions;
if (!cfg || !cfg.ignoreAltSub) {
for (const k in st) if (sink.has(k) && st[k]) for (const g of st[k]) sink.add(g);
} }
} }
function markGlyphsGsubLigature(sink, lookup, cfg) { function markGlyphsGsubMultiple(markedGlyphs, lookup) {
const st = lookup.substitutions;
for (const k in st) {
const d = markedGlyphs.get(k);
if (d && st[k]) for (const g of st[k]) markSingleGlyph(markedGlyphs, g, d + 0x1000);
}
}
function markGlyphsGsubAlternate(markedGlyphs, lookup) {
markGlyphsGsubMultiple(markedGlyphs, lookup);
}
function markGlyphsGsubLigature(markedGlyphs, lookup) {
const st = lookup.substitutions; const st = lookup.substitutions;
for (const sub of st) { for (const sub of st) {
let check = true; let maxD = 0;
for (const g of sub.from) if (!sink.has(g)) check = false; for (const g of sub.from) {
if (check && sub.to) sink.add(sub.to); const d = markedGlyphs.get(g);
if (d && d > maxD) maxD = d;
}
if (maxD && sub.to) markSingleGlyph(markedGlyphs, sub.to, maxD + 0x1000);
} }
} }
function markGlyphsGsubReverse(sink, lookup, cfg) { function markGlyphsGsubReverse(markedGlyphs, lookup) {
for (const rule of lookup.rules) { for (const rule of lookup.rules) {
if (rule.match && rule.to) { if (rule.match && rule.to) {
const matchCoverage = rule.match[rule.inputIndex]; const matchCoverage = rule.match[rule.inputIndex];
for (let j = 0; j < matchCoverage.length; j++) { for (let j = 0; j < matchCoverage.length; j++) {
if (sink.has(matchCoverage[j]) && rule.to[j]) sink.add(rule.to[j]); const d = markedGlyphs.get(matchCoverage[j]);
if (d && rule.to[j]) markSingleGlyph(markedGlyphs, rule.to[j], d + 0x1000);
} }
} }
} }
@ -155,3 +203,181 @@ function markGlyphsGsubReverse(sink, lookup, cfg) {
function sweepGlyphs(glyphStore, gnSet) { function sweepGlyphs(glyphStore, gnSet) {
return glyphStore.filterByName(gnSet); return glyphStore.filterByName(gnSet);
} }
///////////////////////////////////////////////////////////////////////////////////////////////////
function analyzeReferenceGraph(glyphStore, markedGlyphs) {
let depthMap = new Map();
let aliasMap = new Map();
for (const [gn, g] of glyphStore.namedEntries()) {
const d = markedGlyphs.get(gn);
if (d) traverseReferenceTree(depthMap, aliasMap, g, d);
}
aliasMap = optimizeAliasMap(aliasMap, depthMap);
let memo = new Set();
for (const [gn, g] of glyphStore.namedEntries()) {
const d = markedGlyphs.get(gn);
if (d) rectifyGlyphAndMarkComponents(glyphStore, aliasMap, markedGlyphs, memo, g, d);
}
}
// Traverse the glyphs' reference tree and mark the depth of each glyph.
// For aliases (a glyphs which only contains a single reference), mark the aliasing relationship.
function traverseReferenceTree(depthMap, aliasMap, g, d) {
depthMapSet(depthMap, g, d);
let refs = g.geometry.asReferences();
if (!refs) return;
for (const sr of refs) {
traverseReferenceTree(depthMap, aliasMap, sr.glyph, d + 0x10000);
}
if (refs.length === 1) {
const sr = refs[0];
aliasMap.set(g, sr);
}
}
function depthMapSet(depthMap, g, d) {
let existing = depthMap.get(g);
if (null == existing || d < existing) {
depthMap.set(g, d);
return d;
} else {
return existing;
}
}
// Optimize the alias map by altering the geometry of glyphs to reference the "representative glyph",
// which is the glyph with the smallest depth in the cluster of glyphs that aliased to each other.
function optimizeAliasMap(aliasMap, depthMap) {
let collection = collectAliasMap(aliasMap);
resolveCollectionRepresentative(collection, depthMap);
return alterGeometryAndOptimize(collection);
}
// Collect all glyphs into clusters, grouped by the terminal glyph of alias chains.
// Each cluster will contain all the the glyphs that are aliases of the terminal glyph.
function collectAliasMap(aliasMap) {
let aliasResolution = new Map();
for (const g of aliasMap.keys()) {
const terminal = getAliasTerminal(aliasMap, g);
let m = aliasResolution.get(terminal.glyph);
if (!m) {
m = {
representative: null,
aliases: new Map()
};
aliasResolution.set(terminal.glyph, m);
}
m.aliases.set(g, { x: terminal.x, y: terminal.y });
}
for (const [gT, cluster] of aliasResolution) cluster.aliases.set(gT, { x: 0, y: 0 });
return aliasResolution;
}
// Resolve the representative glyph of each cluster, using the glyph with the smallest depth.
function resolveCollectionRepresentative(collection, depthMap) {
for (const [gT, cluster] of collection) {
let d = null;
for (const [g, tf] of cluster.aliases) {
const dt = depthMap.get(g);
if ((d == null && dt != null) || (d != null && dt != null && dt < d)) {
d = dt;
cluster.representative = { glyph: g, x: tf.x, y: tf.y };
}
}
}
}
// Use the collected alias map to alter the geometry of glyphs and produce the optimized alias map.
// The geometry of each glyph will be altered to reference the representative glyph of its cluster,
// while the representative itself's geometry will be the terminal glyph's geometry with translation.
function alterGeometryAndOptimize(collection) {
let optimized = new Map();
for (const [gT, cluster] of collection) {
if (!cluster.representative) {
throw new Error("Unreachable: each cluster should have at least one representative");
}
cluster.representative.glyph.geometry = new Geometry.TransformedGeometry(
gT.geometry,
Transform.Translate(cluster.representative.x, cluster.representative.y)
);
for (const [g, tf] of cluster.aliases) {
if (g != cluster.representative.glyph) {
g.geometry = new Geometry.ReferenceGeometry(
cluster.representative.glyph,
tf.x - cluster.representative.x,
tf.y - cluster.representative.y
);
optimized.set(g, {
glyph: cluster.representative.glyph,
x: tf.x - cluster.representative.x,
y: tf.y - cluster.representative.y
});
}
}
}
return optimized;
}
function getAliasTerminal(aliasMap, g) {
let x = 0,
y = 0;
for (;;) {
const alias = aliasMap.get(g);
if (!alias) {
return { glyph: g, x, y };
} else {
x += alias.x;
y += alias.y;
g = alias.glyph;
}
}
}
function rectifyGlyphAndMarkComponents(glyphStore, aliasMap, markedGlyphs, memo, g, d) {
if (memo.has(g)) return;
memo.add(g);
let refs = g.geometry.asReferences();
if (refs) {
let parts = [];
for (let sr of refs) {
// Resolve alias
const alias = aliasMap.get(sr.glyph);
if (alias) {
sr.glyph = alias.glyph;
sr.x += alias.x;
sr.y += alias.y;
}
const gn = glyphStore.queryNameOf(sr.glyph);
if (!gn) {
// Reference is invalid. The root glyph will be radicalized.
g.geometry = new Geometry.RadicalGeometry(g.geometry.unlinkReferences());
return;
} else {
// Reference is valid. Process the referenced glyph.
if (!markedGlyphs.has(gn)) markedGlyphs.set(gn, d + 0x10000);
rectifyGlyphAndMarkComponents(
glyphStore,
aliasMap,
markedGlyphs,
memo,
sr.glyph,
d + 0x10000
);
parts.push(new Geometry.ReferenceGeometry(sr.glyph, sr.x, sr.y));
}
}
g.geometry = new Geometry.CombineGeometry(parts);
} else {
g.geometry = new Geometry.RadicalGeometry(g.geometry.unlinkReferences());
}
}

View file

@ -41,17 +41,6 @@ function regulateCompositeGlyph(glyphStore, memo, g) {
if (!gn) return memoSet(memo, g, false); if (!gn) return memoSet(memo, g, false);
} }
// De-doppelganger
while (refs.length === 1 && regulateCompositeGlyph(glyphStore, memo, refs[0].glyph)) {
const sr = refs[0];
const targetRefs = sr.glyph.geometry.asReferences();
g.clearGeometry();
for (const tr of targetRefs) {
g.includeGeometry(new Geom.ReferenceGeometry(tr.glyph, tr.x + sr.x, tr.y + sr.y));
}
refs = g.geometry.asReferences();
}
return memoSet(memo, g, true); return memoSet(memo, g, true);
} }

View file

@ -35,7 +35,7 @@ function validateMonospace(para, glyphStore) {
export function finalizeFont(cache, para, glyphStore, excludedCodePoints, restFont) { export function finalizeFont(cache, para, glyphStore, excludedCodePoints, restFont) {
assignGrAndCodeRank(glyphStore, Nwid, Wwid); assignGrAndCodeRank(glyphStore, Nwid, Wwid);
assignSubRank(glyphStore); assignSubRank(glyphStore);
glyphStore = gcFont(glyphStore, excludedCodePoints, restFont, {}); glyphStore = gcFont(glyphStore, excludedCodePoints, restFont);
glyphStore = finalizeGlyphs(cache, para, glyphStore); glyphStore = finalizeGlyphs(cache, para, glyphStore);
validateMonospace(para, glyphStore); validateMonospace(para, glyphStore);
return glyphStore; return glyphStore;

View file

@ -47,8 +47,7 @@ function ConvertGsubGposImpl(handlers, T, table, glyphs) {
} }
} }
for (const l in table.lookups) { for (const l in table.lookups) {
if (!table.lookups[l]) throw new Error(`Cannot find lookup '${l}'`); if (!ls.query(l)) throw new Error("Unreachable: lookupOrder must contain everything");
ls.declare(l, table.lookups[l]);
} }
for (const l in table.lookups) ls.fill(l, table.lookups[l]); for (const l in table.lookups) ls.fill(l, table.lookups[l]);
} }

View file

@ -1,7 +1,7 @@
$$include '../../../meta/macros.ptl' $$include '../../../meta/macros.ptl'
import [mix linreg clamp fallback] from"../../../support/utils.mjs" import [mix linreg clamp fallback] from"../../../support/utils.mjs"
import [DependentSelector CvDecompose MathSansSerif] from"../../../support/gr.mjs" import [DependentSelector MathSansSerif] from"../../../support/gr.mjs"
glyph-module glyph-module

View file

@ -319,6 +319,38 @@ export class TransformedGeometry extends GeometryBase {
} }
} }
export class RadicalGeometry extends GeometryBase {
constructor(g) {
super();
this.m_geom = g;
}
asContours() {
return this.m_geom.asContours();
}
asReferences() {
return null;
}
filterTag(fn) {
const e = this.m_geom.filterTag(fn);
if (!e) return null;
return new RadicalGeometry(e);
}
isEmpty() {
return this.m_geom.isEmpty();
}
measureComplexity() {
return this.m_geom.measureComplexity();
}
unlinkReferences() {
return this.m_geom.unlinkReferences();
}
toShapeStringOrNull() {
const sTarget = this.m_geom.toShapeStringOrNull();
if (!sTarget) return null;
return Format.struct("RadicalGeometry", sTarget);
}
}
export class CombineGeometry extends GeometryBase { export class CombineGeometry extends GeometryBase {
constructor(parts) { constructor(parts) {
super(); super();

View file

@ -1,3 +1,5 @@
import * as util from "util";
import { Anchor } from "../geometry/anchor.mjs"; import { Anchor } from "../geometry/anchor.mjs";
import * as Geom from "../geometry/index.mjs"; import * as Geom from "../geometry/index.mjs";
import { Point, Vec2 } from "../geometry/point.mjs"; import { Point, Vec2 } from "../geometry/point.mjs";
@ -23,6 +25,10 @@ export class Glyph {
this.ctxTag = null; this.ctxTag = null;
} }
[util.inspect.custom](depth, options) {
return options.stylize(this.toString(), "special");
}
toString() { toString() {
return `<Glyph ${this._m_identifier}>`; return `<Glyph ${this._m_identifier}>`;
} }