editor: instrument block ids, table of contents and height map

This commit is contained in:
Ammar Ahmed
2026-08-25 18:35:39 +05:00
parent 53553d859e
commit 82614b4ce3
3 changed files with 47 additions and 28 deletions

View File

@@ -24,6 +24,7 @@ import {
AttributeUpdate,
BatchAttributeStep
} from "../../utils/batch-attribute-step.js";
import { profiler } from "../../utils/profiler.js";
const NESTED_BLOCK_ID_TYPES = ["callout"];
const BLOCK_ID_TYPES = [
@@ -78,40 +79,49 @@ export const BlockId = Extension.create({
const isDocChanged = transactions.some((tr) => tr.docChanged);
if (!isDocChanged) return null;
const blockIds = new Set<string>();
const updates: AttributeUpdate[] = [];
const { tr } = newState;
return profiler.time("blockId.appendTransaction", () => {
const blockIds = new Set<string>();
const updates: AttributeUpdate[] = [];
const { tr } = newState;
let scanned = 0;
tr.doc.forEach(function addBlockId(n, offset) {
if (!n.isBlock || !BLOCK_ID_TYPES.includes(n.type.name)) return;
tr.doc.forEach(function addBlockId(n, offset) {
if (!n.isBlock || !BLOCK_ID_TYPES.includes(n.type.name)) return;
const currentId = n.attrs.blockId;
const shouldUpdateId = !currentId || blockIds.has(currentId);
const finalId = shouldUpdateId ? nanoid(8) : currentId;
scanned++;
const currentId = n.attrs.blockId;
const shouldUpdateId = !currentId || blockIds.has(currentId);
const finalId = shouldUpdateId ? nanoid(8) : currentId;
if (shouldUpdateId) {
updates.push({
pos: offset,
attrName: "blockId",
value: finalId
});
if (shouldUpdateId) {
updates.push({
pos: offset,
attrName: "blockId",
value: finalId
});
}
blockIds.add(finalId);
if (NESTED_BLOCK_ID_TYPES.includes(n.type.name))
n.forEach((n, pos) => addBlockId(n, offset + pos + 1));
});
profiler.count("blockId.blocksScanned", scanned);
profiler.gauge("blockId.blocksPerScan", scanned);
if (updates.length > 0) {
profiler.count("blockId.idsAssigned", updates.length);
profiler.count("blockId.transactionsAppended");
tr.step(new BatchAttributeStep(updates));
tr.setMeta("ignoreEdit", true);
// Transaction.addStep always clears storedMarks
if (newState.storedMarks) tr.setStoredMarks(newState.storedMarks);
return tr;
}
blockIds.add(finalId);
if (NESTED_BLOCK_ID_TYPES.includes(n.type.name))
n.forEach((n, pos) => addBlockId(n, offset + pos + 1));
return null;
});
if (updates.length > 0) {
tr.step(new BatchAttributeStep(updates));
tr.setMeta("ignoreEdit", true);
// Transaction.addStep always clears storedMarks
if (newState.storedMarks) tr.setStoredMarks(newState.storedMarks);
return tr;
}
return null;
}
})
];

View File

@@ -18,6 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { Node as ProsemirrorNode } from "@tiptap/pm/model";
import { profiler } from "../../utils/profiler.js";
const DEFAULT_ESTIMATES: Record<string, number> = {
paragraph: 24,
@@ -49,8 +50,10 @@ export class HeightMap {
heightFor(node: ProsemirrorNode): number {
const blockId = node.attrs.blockId as string | undefined;
if (blockId && this.measured.has(blockId)) {
profiler.count("virtualization.heightMap.hit");
return this.measured.get(blockId) as number;
}
profiler.count("virtualization.heightMap.miss");
return this.estimate(node);
}
@@ -58,6 +61,7 @@ export class HeightMap {
const blockId = node.attrs.blockId as string | undefined;
if (!blockId || !Number.isFinite(height) || height <= 0) return;
this.measured.set(blockId, Math.round(height));
profiler.gauge("virtualization.heightMap.size", this.measured.size);
}
toJSON(): Record<string, number> {

View File

@@ -18,6 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { Node as ProsemirrorNode } from "@tiptap/pm/model";
import { profiler } from "./profiler.js";
export type TOCItem = {
level: number;
@@ -57,6 +58,7 @@ export function getTableOfContents(
doc: ProsemirrorNode,
content: HTMLElement
): TOCItem[] {
const end = profiler.start("toc.build");
const tableOfContents: TOCItem[] = [];
let level = -1;
let prevHeading = 0;
@@ -91,6 +93,9 @@ export function getTableOfContents(
});
});
end();
profiler.count("toc.builds");
profiler.gauge("toc.headings", tableOfContents.length);
return tableOfContents;
}