@milaboratories/pl-middle-layer 1.74.14 → 1.74.16
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/dist/middle_layer/build_stamp.cjs +1 -1
- package/dist/middle_layer/build_stamp.js +1 -1
- package/dist/middle_layer/folders.cjs +10 -6
- package/dist/middle_layer/folders.cjs.map +1 -1
- package/dist/middle_layer/folders.js +10 -6
- package/dist/middle_layer/folders.js.map +1 -1
- package/dist/middle_layer/middle_layer.cjs +39 -18
- package/dist/middle_layer/middle_layer.cjs.map +1 -1
- package/dist/middle_layer/middle_layer.js +39 -18
- package/dist/middle_layer/middle_layer.js.map +1 -1
- package/package.json +2 -2
- package/src/middle_layer/folders.ts +17 -12
- package/src/middle_layer/init_latency_benchmark.test.ts +102 -0
- package/src/middle_layer/middle_layer.ts +77 -55
|
@@ -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
|
-
|
|
156
|
-
|
|
155
|
+
projectsTreeP: Promise<SynchronizedTreeState>,
|
|
156
|
+
templatesTreeP: Promise<SynchronizedTreeState>,
|
|
157
157
|
openedProjects: WatchableValue<ProjectId[]>,
|
|
158
158
|
env: MiddleLayerEnvironment,
|
|
159
159
|
): Promise<TreeAndComputableU<FoldersListing>> {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
)
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
const
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
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
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
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
|
-
|
|
1932
|
+
lazyInit(FoldersField, FoldersResourceType),
|
|
1933
|
+
]);
|
|
1932
1934
|
|
|
1933
|
-
|
|
1935
|
+
await tx.commit();
|
|
1934
1936
|
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
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
|
-
|
|
2024
|
-
|
|
2025
|
-
const
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
projectListTC
|
|
2029
|
-
templateListTC
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
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,
|