@milaboratories/pl-middle-layer 1.74.15 → 1.74.17

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.
@@ -152,21 +152,26 @@ export const foldersFieldFilter: Filter = treeFilter.resourceTypeEq(FoldersResou
152
152
  export async function createFolderList(
153
153
  pl: PlClient,
154
154
  rid: SignedResourceId,
155
- projectsTree: SynchronizedTreeState,
156
- templatesTree: SynchronizedTreeState,
155
+ projectsTreeP: Promise<SynchronizedTreeState>,
156
+ templatesTreeP: Promise<SynchronizedTreeState>,
157
157
  openedProjects: WatchableValue<ProjectId[]>,
158
158
  env: MiddleLayerEnvironment,
159
159
  ): Promise<TreeAndComputableU<FoldersListing>> {
160
- const tree = await SynchronizedTreeState.init(
161
- pl,
162
- rid,
163
- {
164
- ...env.ops.defaultTreeOptions,
165
- pruning: FoldersTreePruningFunction,
166
- fieldFilter: foldersFieldFilter,
167
- },
168
- env.logger,
169
- );
160
+ // The other two trees are promises so the folder tree loads alongside them, not after them.
161
+ const [tree, projectsTree, templatesTree] = await Promise.all([
162
+ SynchronizedTreeState.init(
163
+ pl,
164
+ rid,
165
+ {
166
+ ...env.ops.defaultTreeOptions,
167
+ pruning: FoldersTreePruningFunction,
168
+ fieldFilter: foldersFieldFilter,
169
+ },
170
+ env.logger,
171
+ ),
172
+ projectsTreeP,
173
+ templatesTreeP,
174
+ ]);
170
175
 
171
176
  const c = Computable.make((ctx) => {
172
177
  const projectsNode = ctx.accessor(projectsTree.entry()).node();
@@ -0,0 +1,102 @@
1
+ import net from "node:net";
2
+ import path from "path";
3
+ import { randomUUID } from "node:crypto";
4
+ import type { PlClient } from "@milaboratories/pl-client";
5
+ import { plAddressToConfig, TestHelpers } from "@milaboratories/pl-client";
6
+ import { test } from "vitest";
7
+ import { MiddleLayer } from "./middle_layer";
8
+
9
+ // Times MiddleLayer.init against a local http backend through a proxy that delays every chunk by
10
+ // PL_PROXY_LATENCY_MS each way (default 25). No-ops without PL_ML_INIT_BENCH=1:
11
+ // PL_ML_INIT_BENCH=1 PL_PROXY_LATENCY_MS=50 pnpm vitest run src/middle_layer/init_latency_benchmark.test.ts
12
+ test.skipIf(process.env.PL_ML_INIT_BENCH !== "1")(
13
+ "MiddleLayer.init latency",
14
+ { timeout: 300_000 },
15
+ async () => {
16
+ const { hostAndPort, ssl } = plAddressToConfig(TestHelpers.getTestConfig().address);
17
+ const [host, port] = hostAndPort.split(":");
18
+ if (ssl || (host !== "127.0.0.1" && host !== "localhost")) {
19
+ console.warn("*** skipping benchmark, PL_ADDRESS is not a local http backend", hostAndPort);
20
+ return;
21
+ }
22
+ const proxy = await startLinkProxy(
23
+ host,
24
+ Number(port),
25
+ Number(process.env.PL_PROXY_LATENCY_MS ?? 25),
26
+ );
27
+ // Only the dialed address changes; everything else PL_ADDRESS sets (timeouts, auth) still applies.
28
+ const viaProxy = { hostAndPort: `127.0.0.1:${proxy.port}` };
29
+ const tempRoot = `test_${Date.now()}_${randomUUID()}`;
30
+ const totals: number[] = [];
31
+ try {
32
+ // Run 0 is a first-time user on a fresh temp root. The rest run on the test user's own root,
33
+ // whose singletons persist like a returning user's (run 1 too may create them on a fresh
34
+ // backend), and they load whatever other tests left there, so they vary between machines.
35
+ for (let run = 0; run < 5; run++) {
36
+ const client = await TestHelpers.getTestClient(run === 0 ? tempRoot : undefined, viaProxy);
37
+ if (run === 0) await logPingRtt(client);
38
+ const t0 = performance.now();
39
+ const stamp = (msg: unknown) =>
40
+ console.log(` +${(performance.now() - t0).toFixed(0).padStart(5)}ms ${String(msg)}`);
41
+ let ml: MiddleLayer;
42
+ try {
43
+ ml = await MiddleLayer.init(client, path.resolve(`work/${randomUUID()}`), {
44
+ localSecret: MiddleLayer.generateLocalSecret(),
45
+ localProjections: [],
46
+ openFileDialogCallback: () => {
47
+ throw new Error("Not implemented.");
48
+ },
49
+ logger: { info: stamp, warn: stamp, error: stamp },
50
+ });
51
+ } catch (e) {
52
+ await client.close();
53
+ throw e;
54
+ }
55
+ try {
56
+ totals.push(performance.now() - t0);
57
+ stamp(`run ${run}: init done`);
58
+ } finally {
59
+ // Closes the client too.
60
+ await ml.close();
61
+ }
62
+ }
63
+ } finally {
64
+ proxy.close();
65
+ const owner = await TestHelpers.getTestClient();
66
+ try {
67
+ await owner.deleteAlternativeRoot(tempRoot);
68
+ } finally {
69
+ await owner.close();
70
+ }
71
+ }
72
+ console.log(`init ms per run: ${totals.map((t) => t.toFixed(0)).join(" ")}`);
73
+ },
74
+ );
75
+
76
+ async function logPingRtt(pl: PlClient) {
77
+ const rtts: number[] = [];
78
+ for (let i = 0; i < 5; i++) {
79
+ const t = performance.now();
80
+ await pl.ping();
81
+ rtts.push(performance.now() - t);
82
+ }
83
+ console.log(`ping rtt ms: ${rtts.map((r) => r.toFixed(0)).join(" ")}`);
84
+ }
85
+
86
+ /** A TCP proxy behaving like a link with a fixed one-way delay: every chunk is forwarded `latency`
87
+ * ms after it arrives, so back-to-back chunks overlap instead of queueing behind each other. */
88
+ async function startLinkProxy(targetHost: string, targetPort: number, latency: number) {
89
+ const delayed = (from: net.Socket, to: net.Socket) => {
90
+ from.on("data", (chunk) => setTimeout(() => to.write(chunk), latency));
91
+ from.on("end", () => setTimeout(() => to.end(), latency));
92
+ from.on("error", () => to.destroy());
93
+ };
94
+ const server = net.createServer((socket) => {
95
+ const upstream = net.connect(targetPort, targetHost);
96
+ delayed(socket, upstream);
97
+ delayed(upstream, socket);
98
+ });
99
+ await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
100
+ const port = (server.address() as net.AddressInfo).port;
101
+ return { port, close: () => server.close() };
102
+ }
@@ -1898,55 +1898,65 @@ export class MiddleLayer {
1898
1898
  )
1899
1899
  ops.defaultTreeOptions.traversalMode = getDebugFlags().treeTraversalMode;
1900
1900
 
1901
- const { projects, templates, sharingOutbox, sharingState, folders } = await pl.withWriteTx(
1902
- "MLInitialization",
1903
- async (tx) => {
1904
- // Lazily create each clientRoot-attached singleton resource. Returns the existing
1905
- // resource id if the field is already populated, otherwise creates + locks + sets it.
1906
- // A created resource's id is known only once the transaction commits.
1907
- type Singleton = { existing: SignedResourceId } | { ref: ResourceRef };
1908
- const lazyInit = async (
1909
- fieldName: string,
1910
- type: { name: string; version: string },
1911
- ): Promise<Singleton> => {
1912
- const f = field(tx.clientRoot, fieldName);
1913
- tx.createField(f, "Dynamic");
1914
- const fData = await tx.getField(f);
1915
- if (isNullSignedResourceId(fData.value)) {
1916
- const ref = tx.createEphemeral(type);
1917
- tx.lock(ref);
1918
- tx.setField(f, ref);
1919
- return { ref };
1920
- }
1921
- return { existing: fData.value };
1922
- };
1901
+ const initTxP = pl.withWriteTx("MLInitialization", async (tx) => {
1902
+ // Lazily create each clientRoot-attached singleton resource. Returns the existing
1903
+ // resource id if the field is already populated, otherwise creates + locks + sets it.
1904
+ // A created resource's id is known only once the transaction commits.
1905
+ type Singleton = { existing: SignedResourceId } | { ref: ResourceRef };
1906
+ const lazyInit = async (
1907
+ fieldName: string,
1908
+ type: { name: string; version: string },
1909
+ ): Promise<Singleton> => {
1910
+ const f = field(tx.clientRoot, fieldName);
1911
+ tx.createField(f, "Dynamic");
1912
+ const fData = await tx.getField(f);
1913
+ if (isNullSignedResourceId(fData.value)) {
1914
+ const ref = tx.createEphemeral(type);
1915
+ tx.lock(ref);
1916
+ tx.setField(f, ref);
1917
+ return { ref };
1918
+ }
1919
+ return { existing: fData.value };
1920
+ };
1923
1921
 
1924
- const projectsR = await lazyInit(ProjectsField, ProjectsResourceType);
1925
- const templatesR = await lazyInit(TemplatesField, TemplatesResourceType);
1926
- const outboxR = await lazyInit(SharingOutboxField, SharingOutboxResourceType);
1927
- const stateR = await lazyInit(SharingStateField, SharingStateResourceType);
1922
+ // All five in flight at once: the transaction pipelines its requests, so this costs one
1923
+ // round trip instead of five.
1924
+ const [projectsR, templatesR, outboxR, stateR, foldersR] = await Promise.all([
1925
+ lazyInit(ProjectsField, ProjectsResourceType),
1926
+ lazyInit(TemplatesField, TemplatesResourceType),
1927
+ lazyInit(SharingOutboxField, SharingOutboxResourceType),
1928
+ lazyInit(SharingStateField, SharingStateResourceType),
1928
1929
  // The folder tree gets its own root-attached singleton rather than a field on the
1929
1930
  // projects resource: an extra field there is walked by the released project-list reader
1930
1931
  // and dereferenced as a project, which takes the whole list down, not just the folders.
1931
- const foldersR = await lazyInit(FoldersField, FoldersResourceType);
1932
+ lazyInit(FoldersField, FoldersResourceType),
1933
+ ]);
1932
1934
 
1933
- await tx.commit();
1935
+ await tx.commit();
1934
1936
 
1935
- const idOf = async (r: Singleton) => ("existing" in r ? r.existing : await r.ref.globalId);
1936
- return {
1937
- projects: await idOf(projectsR),
1938
- templates: await idOf(templatesR),
1939
- sharingState: await idOf(stateR),
1940
- sharingOutbox: await idOf(outboxR),
1941
- folders: await idOf(foldersR),
1942
- };
1943
- },
1944
- );
1937
+ const idOf = async (r: Singleton) => ("existing" in r ? r.existing : await r.ref.globalId);
1938
+ const [projects, templates, sharingState, sharingOutbox, folders] = await Promise.all([
1939
+ idOf(projectsR),
1940
+ idOf(templatesR),
1941
+ idOf(stateR),
1942
+ idOf(outboxR),
1943
+ idOf(foldersR),
1944
+ ]);
1945
+ return { projects, templates, sharingState, sharingOutbox, folders };
1946
+ });
1947
+ // The driver kit needs nothing from the transaction, so it starts alongside it.
1948
+ const driverKitP = initDriverKit(pl, workdir, ops.frontendDownloadPath, ops);
1949
+ const [initTx, driverKitR] = await Promise.allSettled([initTxP, driverKitP]);
1950
+ if (initTx.status === "rejected") {
1951
+ if (driverKitR.status === "fulfilled") await driverKitR.value.dispose();
1952
+ throw initTx.reason;
1953
+ }
1954
+ if (driverKitR.status === "rejected") throw driverKitR.reason;
1955
+ const { projects, templates, sharingOutbox, sharingState, folders } = initTx.value;
1956
+ const driverKit = driverKitR.value;
1945
1957
 
1946
1958
  const logger = ops.logger;
1947
1959
 
1948
- const driverKit = await initDriverKit(pl, workdir, ops.frontendDownloadPath, ops);
1949
-
1950
1960
  // passed to components having no own retry logic
1951
1961
  const retryHttpDispatcher = new RetryAgent(pl.httpDispatcher);
1952
1962
 
@@ -2020,21 +2030,33 @@ export class MiddleLayer {
2020
2030
  };
2021
2031
 
2022
2032
  const openedProjects = new WatchableValue<ProjectId[]>([]);
2023
- const projectListTC = await createProjectList(pl, projects, openedProjects, env);
2024
- const templateListTC = await createTemplateList(pl, templates, env);
2025
- const foldersTC = await createFolderList(
2026
- pl,
2027
- folders,
2028
- projectListTC.tree,
2029
- templateListTC.tree,
2030
- openedProjects,
2031
- env,
2032
- );
2033
-
2034
- // Project sharing trees and reactive views.
2035
- const outgoingTC = await createOutgoingShares(pl, sharingOutbox, env);
2036
- const sharingStateTree = await createSharingStateTree(pl, sharingState, env);
2037
- const availableSharesTree = await createAvailableSharesTree(pl, env);
2033
+ // Each tree's first load is a round trip of its own; they are independent, so they all load
2034
+ // at once rather than one after another.
2035
+ const projectListP = createProjectList(pl, projects, openedProjects, env);
2036
+ const templateListP = createTemplateList(pl, templates, env);
2037
+ const [
2038
+ projectListTC,
2039
+ templateListTC,
2040
+ foldersTC,
2041
+ // Project sharing trees and reactive views.
2042
+ outgoingTC,
2043
+ sharingStateTree,
2044
+ availableSharesTree,
2045
+ ] = await Promise.all([
2046
+ projectListP,
2047
+ templateListP,
2048
+ createFolderList(
2049
+ pl,
2050
+ folders,
2051
+ projectListP.then((tc) => tc.tree),
2052
+ templateListP.then((tc) => tc.tree),
2053
+ openedProjects,
2054
+ env,
2055
+ ),
2056
+ createOutgoingShares(pl, sharingOutbox, env),
2057
+ createSharingStateTree(pl, sharingState, env),
2058
+ createAvailableSharesTree(pl, env),
2059
+ ]);
2038
2060
  const availableShares = createAvailableSharesComputable(
2039
2061
  availableSharesTree,
2040
2062
  sharingStateTree,