Skip to content

Commit 343dc11

Browse files
committed
ENG-1616: Bulk-read settings + thread snapshot (with timing logs)
Cut plugin load from ~20925ms to ~1327ms (94%) on a real graph by collapsing per-call settings accessors into a single bulk read at init and threading that snapshot through the init chain + observer callbacks. Key changes: - accessors.ts: bulkReadSettings() runs ONE pull query against the settings page's direct children and returns { featureFlags, globalSettings, personalSettings } parsed via Zod. readPathValue exported. - getDiscourseNodes / getDiscourseRelations / getAllRelations: optional snapshot param threaded through, no breaking changes to existing callers. - initializeDiscourseNodes + refreshConfigTree (+ registerDiscourseDatalog- Translators, getDiscourseRelationLabels): accept and forward snapshot. - index.ts: bulkReadSettings() at the top of init; snapshot threaded into initializeDiscourseNodes, refreshConfigTree, initObservers, installDiscourseFloatingMenu, setInitialQueryPages, and the 3 sync sites inside index.ts itself. - initializeObserversAndListeners.ts: snapshot threaded into the sync-init body; pageTitleObserver + leftSidebarObserver callbacks call bulkReadSettings() per fire (fresh, not stale); nodeTagPopupButtonObserver uses per-sync-batch memoization via queueMicrotask; hashChangeListener and nodeCreationPopoverListener use bulkReadSettings() per fire. - findDiscourseNode: snapshot param added; getDiscourseNodes() default-arg moved inside the cache-miss branch so cache hits don't waste the call. - isQueryPage / isCanvasPage / QueryPagesPanel.getQueryPages: optional snapshot param. - LeftSidebarView.buildConfig / useConfig / mountLeftSidebar: optional initialSnapshot threaded for the first render; emitter-driven updates keep using live reads for post-mount reactivity. - DiscourseFloatingMenu.installDiscourseFloatingMenu: optional snapshot. - posthog.initPostHog: removed redundant internal getPersonalSetting check (caller already guards from the snapshot). - migrateLegacyToBlockProps.hasGraphMigrationMarker: accepts the existing blockMap and does an O(1) lookup instead of a getBlockUidByTextOnPage scan. Includes per-phase timing console.logs across index.ts, refreshConfigTree, init.ts, initSettingsPageBlocks, and initObservers. Committed as a checkpoint so we can reference measurements later; will be removed in the next commit.
1 parent 86cf3f0 commit 343dc11

19 files changed

Lines changed: 441 additions & 120 deletions

apps/roam/src/components/DiscourseFloatingMenu.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@ import {
1313
import { FeedbackWidget } from "./BirdEatsBugs";
1414
import { render as renderSettings } from "~/components/settings/Settings";
1515
import posthog from "posthog-js";
16-
import { getPersonalSetting } from "./settings/utils/accessors";
16+
import {
17+
getPersonalSetting,
18+
type SettingsSnapshot,
19+
} from "./settings/utils/accessors";
1720
import { PERSONAL_KEYS } from "./settings/utils/settingKeys";
1821

1922
type DiscourseFloatingMenuProps = {
@@ -118,6 +121,7 @@ export const showDiscourseFloatingMenu = () => {
118121

119122
export const installDiscourseFloatingMenu = (
120123
onLoadArgs: OnloadArgs,
124+
snapshot?: SettingsSnapshot,
121125
props: DiscourseFloatingMenuProps = {
122126
position: "bottom-right",
123127
theme: "bp3-light",
@@ -130,7 +134,10 @@ export const installDiscourseFloatingMenu = (
130134
floatingMenuAnchor.id = ANCHOR_ID;
131135
document.getElementById("app")?.appendChild(floatingMenuAnchor);
132136
}
133-
if (getPersonalSetting<boolean>([PERSONAL_KEYS.hideFeedbackButton])) {
137+
const hideFeedbackButton = snapshot
138+
? snapshot.personalSettings[PERSONAL_KEYS.hideFeedbackButton]
139+
: getPersonalSetting<boolean>([PERSONAL_KEYS.hideFeedbackButton]);
140+
if (hideFeedbackButton) {
134141
floatingMenuAnchor.classList.add("hidden");
135142
}
136143
ReactDOM.render(

apps/roam/src/components/LeftSidebarView.tsx

Lines changed: 41 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import {
3939
getPersonalSettings,
4040
setGlobalSetting,
4141
setPersonalSetting,
42+
type SettingsSnapshot,
4243
} from "~/components/settings/utils/accessors";
4344
import {
4445
PERSONAL_KEYS,
@@ -79,6 +80,10 @@ const truncate = (s: string, max: number | undefined): string => {
7980
};
8081

8182
const openTarget = async (e: React.MouseEvent, targetUid: string) => {
83+
const _navStart = performance.now();
84+
console.log(
85+
`[DG Nav] openTarget click t=${Math.round(_navStart)} target=${targetUid}`,
86+
);
8287
e.preventDefault();
8388
e.stopPropagation();
8489
const target = parseReference(targetUid);
@@ -89,11 +94,17 @@ const openTarget = async (e: React.MouseEvent, targetUid: string) => {
8994
if (target.type === "block") {
9095
if (e.shiftKey) {
9196
await openBlockInSidebar(target.uid);
97+
console.log(
98+
`[DG Nav] openBlockInSidebar resolved +${Math.round(performance.now() - _navStart)}ms`,
99+
);
92100
return;
93101
}
94102
await window.roamAlphaAPI.ui.mainWindow.openBlock({
95103
block: { uid: target.uid },
96104
});
105+
console.log(
106+
`[DG Nav] openBlock resolved +${Math.round(performance.now() - _navStart)}ms`,
107+
);
97108
return;
98109
}
99110

@@ -103,10 +114,16 @@ const openTarget = async (e: React.MouseEvent, targetUid: string) => {
103114
// eslint-disable-next-line @typescript-eslint/naming-convention
104115
window: { type: "outline", "block-uid": targetUid },
105116
});
117+
console.log(
118+
`[DG Nav] rightSidebar.addWindow resolved +${Math.round(performance.now() - _navStart)}ms`,
119+
);
106120
} else {
107121
await window.roamAlphaAPI.ui.mainWindow.openPage({
108122
page: { uid: targetUid },
109123
});
124+
console.log(
125+
`[DG Nav] openPage resolved +${Math.round(performance.now() - _navStart)}ms`,
126+
);
110127
}
111128
};
112129

@@ -336,14 +353,16 @@ const GlobalSection = ({ config }: { config: LeftSidebarConfig["global"] }) => {
336353

337354
// TODO(ENG-1471): Remove old-system merge when migration complete — just use accessor values directly.
338355
// See mergeGlobalSectionWithAccessor/mergePersonalSectionsWithAccessor for why the merge exists.
339-
const buildConfig = (): LeftSidebarConfig => {
356+
const buildConfig = (snapshot?: SettingsSnapshot): LeftSidebarConfig => {
340357
// Read VALUES from accessor (handles flag routing + mismatch detection)
341-
const globalValues = getGlobalSetting<LeftSidebarGlobalSettings>([
342-
GLOBAL_KEYS.leftSidebar,
343-
]);
344-
const personalValues = getPersonalSetting<
345-
ReturnType<typeof getPersonalSettings>[typeof PERSONAL_KEYS.leftSidebar]
346-
>([PERSONAL_KEYS.leftSidebar]);
358+
const globalValues = snapshot
359+
? snapshot.globalSettings[GLOBAL_KEYS.leftSidebar]
360+
: getGlobalSetting<LeftSidebarGlobalSettings>([GLOBAL_KEYS.leftSidebar]);
361+
const personalValues = snapshot
362+
? snapshot.personalSettings[PERSONAL_KEYS.leftSidebar]
363+
: getPersonalSetting<
364+
ReturnType<typeof getPersonalSettings>[typeof PERSONAL_KEYS.leftSidebar]
365+
>([PERSONAL_KEYS.leftSidebar]);
347366

348367
// Read UIDs from old system (needed for fold CRUD during dual-write)
349368
const oldConfig = getCurrentLeftSidebarConfig();
@@ -364,8 +383,8 @@ const buildConfig = (): LeftSidebarConfig => {
364383
};
365384
};
366385

367-
export const useConfig = () => {
368-
const [config, setConfig] = useState(() => buildConfig());
386+
export const useConfig = (initialSnapshot?: SettingsSnapshot) => {
387+
const [config, setConfig] = useState(() => buildConfig(initialSnapshot));
369388
useEffect(() => {
370389
const handleUpdate = () => {
371390
setConfig(buildConfig());
@@ -504,8 +523,14 @@ const FavoritesPopover = ({ onloadArgs }: { onloadArgs: OnloadArgs }) => {
504523
);
505524
};
506525

507-
const LeftSidebarView = ({ onloadArgs }: { onloadArgs: OnloadArgs }) => {
508-
const { config } = useConfig();
526+
const LeftSidebarView = ({
527+
onloadArgs,
528+
initialSnapshot,
529+
}: {
530+
onloadArgs: OnloadArgs;
531+
initialSnapshot?: SettingsSnapshot;
532+
}) => {
533+
const { config } = useConfig(initialSnapshot);
509534

510535
return (
511536
<>
@@ -613,6 +638,7 @@ const migrateFavorites = async () => {
613638
export const mountLeftSidebar = async (
614639
wrapper: HTMLElement,
615640
onloadArgs: OnloadArgs,
641+
initialSnapshot?: SettingsSnapshot,
616642
): Promise<void> => {
617643
if (!wrapper) return;
618644

@@ -630,7 +656,10 @@ export const mountLeftSidebar = async (
630656
} else {
631657
root.className = "starred-pages";
632658
}
633-
ReactDOM.render(<LeftSidebarView onloadArgs={onloadArgs} />, root);
659+
ReactDOM.render(
660+
<LeftSidebarView onloadArgs={onloadArgs} initialSnapshot={initialSnapshot} />,
661+
root,
662+
);
634663
};
635664

636665
export default LeftSidebarView;

apps/roam/src/components/settings/QueryPagesPanel.tsx

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ import React, { useState } from "react";
44
import type { OnloadArgs } from "roamjs-components/types";
55
import {
66
getPersonalSetting,
7+
readPathValue,
78
setPersonalSetting,
9+
type SettingsSnapshot,
810
} from "~/components/settings/utils/accessors";
911
import {
1012
PERSONAL_KEYS,
@@ -13,11 +15,16 @@ import {
1315

1416
// Legacy extensionAPI stored query-pages as string | string[] | Record<string, string>.
1517
// Coerce to string[] for backward compatibility with old stored formats.
16-
export const getQueryPages = (): string[] => {
17-
const value = getPersonalSetting<string[] | string | Record<string, string>>([
18-
PERSONAL_KEYS.query,
19-
QUERY_KEYS.queryPages,
20-
]);
18+
export const getQueryPages = (snapshot?: SettingsSnapshot): string[] => {
19+
const value = snapshot
20+
? (readPathValue(snapshot.personalSettings, [
21+
PERSONAL_KEYS.query,
22+
QUERY_KEYS.queryPages,
23+
]) as string[] | string | Record<string, string> | undefined)
24+
: getPersonalSetting<string[] | string | Record<string, string>>([
25+
PERSONAL_KEYS.query,
26+
QUERY_KEYS.queryPages,
27+
]);
2128
return typeof value === "string"
2229
? [value]
2330
: Array.isArray(value)

apps/roam/src/components/settings/utils/accessors.ts

Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ const getSchemaAtPath = (
143143
const formatSettingPath = (keys: string[]): string =>
144144
keys.length === 0 ? "(root)" : keys.join(" > ");
145145

146-
const readPathValue = (root: unknown, keys: string[]): unknown =>
146+
export const readPathValue = (root: unknown, keys: string[]): unknown =>
147147
keys.reduce<unknown>((current, key) => {
148148
if (Array.isArray(current)) {
149149
const index = Number(key);
@@ -863,8 +863,10 @@ export const setGlobalSetting = (keys: string[], value: json): void => {
863863
});
864864
};
865865

866-
export const getAllRelations = (): DiscourseRelation[] => {
867-
const settings = getGlobalSettings();
866+
export const getAllRelations = (
867+
snapshot?: SettingsSnapshot,
868+
): DiscourseRelation[] => {
869+
const settings = snapshot ? snapshot.globalSettings : getGlobalSettings();
868870

869871
return Object.entries(settings.Relations).flatMap(([id, relation]) =>
870872
relation.ifConditions.map((ifCondition) => ({
@@ -909,6 +911,61 @@ export const getPersonalSetting = <T = unknown>(
909911
return blockPropsValue as T | undefined;
910912
};
911913

914+
export type SettingsSnapshot = {
915+
featureFlags: FeatureFlags;
916+
globalSettings: GlobalSettings;
917+
personalSettings: PersonalSettings;
918+
};
919+
920+
export const bulkReadSettings = (): SettingsSnapshot => {
921+
const start = performance.now();
922+
923+
const pageResult = window.roamAlphaAPI.pull(
924+
"[{:block/children [:block/string :block/props]}]",
925+
[":node/title", DG_BLOCK_PROP_SETTINGS_PAGE_TITLE],
926+
) as Record<string, json> | null;
927+
const afterQuery = performance.now();
928+
929+
const children = (pageResult?.[":block/children"] ?? []) as Record<
930+
string,
931+
json
932+
>[];
933+
const personalKey = getPersonalSettingsKey();
934+
let featureFlagsProps: json = {};
935+
let globalProps: json = {};
936+
let personalProps: json = {};
937+
938+
for (const child of children) {
939+
const text = child[":block/string"];
940+
if (typeof text !== "string") continue;
941+
const rawBlockProps = child[":block/props"];
942+
const blockProps =
943+
rawBlockProps && typeof rawBlockProps === "object"
944+
? normalizeProps(rawBlockProps)
945+
: {};
946+
if (text === TOP_LEVEL_BLOCK_PROP_KEYS.featureFlags) {
947+
featureFlagsProps = blockProps;
948+
} else if (text === TOP_LEVEL_BLOCK_PROP_KEYS.global) {
949+
globalProps = blockProps;
950+
} else if (text === personalKey) {
951+
personalProps = blockProps;
952+
}
953+
}
954+
955+
const snapshot: SettingsSnapshot = {
956+
featureFlags: FeatureFlagsSchema.parse(featureFlagsProps || {}),
957+
globalSettings: GlobalSettingsSchema.parse(globalProps || {}),
958+
personalSettings: PersonalSettingsSchema.parse(personalProps || {}),
959+
};
960+
961+
const end = performance.now();
962+
console.log(
963+
`[DG Plugin] bulkReadSettings: ${Math.round(end - start)}ms (query ${Math.round(afterQuery - start)}ms, parse ${Math.round(end - afterQuery)}ms)`,
964+
);
965+
966+
return snapshot;
967+
};
968+
912969
export const setPersonalSetting = (keys: string[], value: json): void => {
913970
if (keys.length === 0) {
914971
internalError({

apps/roam/src/components/settings/utils/init.ts

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -189,15 +189,29 @@ const initializeSettingsBlockProps = (
189189
};
190190

191191
const initSettingsPageBlocks = async (): Promise<Record<string, string>> => {
192+
let t = performance.now();
193+
const mark = (label: string) => {
194+
const now = performance.now();
195+
console.log(
196+
`[DG Plugin] initSettingsPageBlocks.${label}: ${Math.round(now - t)}ms`,
197+
);
198+
t = now;
199+
};
200+
192201
const pageUid = await ensurePageExists(DG_BLOCK_PROP_SETTINGS_PAGE_TITLE);
202+
mark("ensurePageExists");
193203
const blockMap = buildBlockMap(pageUid);
204+
mark("buildBlockMap");
194205

195206
const topLevelBlocks = getTopLevelBlockPropsConfig().map(({ key }) => key);
196207
await ensureBlocksExist(pageUid, topLevelBlocks, blockMap);
208+
mark("ensureBlocksExist (top-level)");
197209

198210
await ensureLegacyConfigBlocks(pageUid);
211+
mark("ensureLegacyConfigBlocks");
199212

200213
initializeSettingsBlockProps(pageUid, blockMap);
214+
mark("initializeSettingsBlockProps");
201215

202216
return blockMap;
203217
};
@@ -411,16 +425,25 @@ const logDualReadComparison = (): void => {
411425
};
412426

413427
export const initSchema = async (): Promise<InitSchemaResult> => {
428+
console.log("[DG Plugin] Initializing schema...");
429+
let t = performance.now();
430+
const mark = (label: string) => {
431+
const now = performance.now();
432+
console.log(`[DG Plugin] initSchema.${label}: ${Math.round(now - t)}ms`);
433+
t = now;
434+
};
435+
414436
const blockUids = await initSettingsPageBlocks();
437+
mark("initSettingsPageBlocks");
438+
415439
await migrateGraphLevel(blockUids);
440+
mark("migrateGraphLevel");
441+
416442
const nodePageUids = await initDiscourseNodePages();
443+
mark("initDiscourseNodePages");
444+
417445
await migratePersonalSettings(blockUids);
418-
try {
419-
logDualReadComparison();
420-
} catch (e) {
421-
console.warn("[DG Dual-Read] Comparison failed:", e);
422-
}
423-
(window as unknown as Record<string, unknown>).dgDualReadLog =
424-
logDualReadComparison;
446+
mark("migratePersonalSettings");
447+
425448
return { blockUids, nodePageUids };
426449
};

apps/roam/src/components/settings/utils/migrateLegacyToBlockProps.ts

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import getBlockProps from "~/utils/getBlockProps";
22
import type { json } from "~/utils/getBlockProps";
33
import setBlockProps from "~/utils/setBlockProps";
4-
import getBlockUidByTextOnPage from "roamjs-components/queries/getBlockUidByTextOnPage";
54
import getPageUidByPageTitle from "roamjs-components/queries/getPageUidByPageTitle";
65
import { createBlock } from "roamjs-components/writes";
76
import { getSetting, setSetting } from "~/utils/extensionSettings";
@@ -29,11 +28,8 @@ const GRAPH_MIGRATION_MARKER = "Block props migrated";
2928
const PERSONAL_MIGRATION_MARKER = "dg-personal-settings-migrated";
3029
const MAX_ERROR_CONTEXT_LENGTH = 5000;
3130

32-
const hasGraphMigrationMarker = (): boolean =>
33-
!!getBlockUidByTextOnPage({
34-
text: GRAPH_MIGRATION_MARKER,
35-
title: DG_BLOCK_PROP_SETTINGS_PAGE_TITLE,
36-
});
31+
const hasGraphMigrationMarker = (blockMap: Record<string, string>): boolean =>
32+
!!blockMap[GRAPH_MIGRATION_MARKER];
3733

3834
const isPropsValid = (
3935
schema: z.ZodTypeAny,
@@ -182,7 +178,7 @@ export const migrateGraphLevel = async (
182178
return;
183179
}
184180

185-
if (hasGraphMigrationMarker()) {
181+
if (hasGraphMigrationMarker(blockUids)) {
186182
console.log(`${LOG_PREFIX} graph-level: skipped (already migrated)`);
187183
return;
188184
}

0 commit comments

Comments
 (0)