@milaboratories/pf-driver 1.0.0
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/README.md +1 -0
- package/dist/__external/.pnpm/@rollup_plugin-typescript@12.1.4_rollup@4.52.4_tslib@2.7.0_typescript@5.6.3/__external/tslib/tslib.es6.cjs +77 -0
- package/dist/__external/.pnpm/@rollup_plugin-typescript@12.1.4_rollup@4.52.4_tslib@2.7.0_typescript@5.6.3/__external/tslib/tslib.es6.cjs.map +1 -0
- package/dist/__external/.pnpm/@rollup_plugin-typescript@12.1.4_rollup@4.52.4_tslib@2.7.0_typescript@5.6.3/__external/tslib/tslib.es6.js +74 -0
- package/dist/__external/.pnpm/@rollup_plugin-typescript@12.1.4_rollup@4.52.4_tslib@2.7.0_typescript@5.6.3/__external/tslib/tslib.es6.js.map +1 -0
- package/dist/data_info_helpers.cjs +21 -0
- package/dist/data_info_helpers.cjs.map +1 -0
- package/dist/data_info_helpers.d.ts +3 -0
- package/dist/data_info_helpers.d.ts.map +1 -0
- package/dist/data_info_helpers.js +19 -0
- package/dist/data_info_helpers.js.map +1 -0
- package/dist/driver_decl.d.ts +38 -0
- package/dist/driver_decl.d.ts.map +1 -0
- package/dist/driver_double.cjs +98 -0
- package/dist/driver_double.cjs.map +1 -0
- package/dist/driver_double.d.ts +12 -0
- package/dist/driver_double.d.ts.map +1 -0
- package/dist/driver_double.js +95 -0
- package/dist/driver_double.js.map +1 -0
- package/dist/driver_impl.cjs +378 -0
- package/dist/driver_impl.cjs.map +1 -0
- package/dist/driver_impl.d.ts +54 -0
- package/dist/driver_impl.d.ts.map +1 -0
- package/dist/driver_impl.js +375 -0
- package/dist/driver_impl.js.map +1 -0
- package/dist/index.cjs +14 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/logging.cjs +9 -0
- package/dist/logging.cjs.map +1 -0
- package/dist/logging.d.ts +2 -0
- package/dist/logging.d.ts.map +1 -0
- package/dist/logging.js +7 -0
- package/dist/logging.js.map +1 -0
- package/dist/pframe_pool.cjs +196 -0
- package/dist/pframe_pool.cjs.map +1 -0
- package/dist/pframe_pool.d.ts +35 -0
- package/dist/pframe_pool.d.ts.map +1 -0
- package/dist/pframe_pool.js +193 -0
- package/dist/pframe_pool.js.map +1 -0
- package/dist/ptable_cache_per_frame.cjs +69 -0
- package/dist/ptable_cache_per_frame.cjs.map +1 -0
- package/dist/ptable_cache_per_frame.d.ts +25 -0
- package/dist/ptable_cache_per_frame.d.ts.map +1 -0
- package/dist/ptable_cache_per_frame.js +66 -0
- package/dist/ptable_cache_per_frame.js.map +1 -0
- package/dist/ptable_cache_plain.cjs +54 -0
- package/dist/ptable_cache_plain.cjs.map +1 -0
- package/dist/ptable_cache_plain.d.ts +21 -0
- package/dist/ptable_cache_plain.d.ts.map +1 -0
- package/dist/ptable_cache_plain.js +51 -0
- package/dist/ptable_cache_plain.js.map +1 -0
- package/dist/ptable_def_pool.cjs +53 -0
- package/dist/ptable_def_pool.cjs.map +1 -0
- package/dist/ptable_def_pool.d.ts +21 -0
- package/dist/ptable_def_pool.d.ts.map +1 -0
- package/dist/ptable_def_pool.js +50 -0
- package/dist/ptable_def_pool.js.map +1 -0
- package/dist/ptable_pool.cjs +167 -0
- package/dist/ptable_pool.cjs.map +1 -0
- package/dist/ptable_pool.d.ts +26 -0
- package/dist/ptable_pool.d.ts.map +1 -0
- package/dist/ptable_pool.js +164 -0
- package/dist/ptable_pool.js.map +1 -0
- package/dist/ptable_shared.cjs +10 -0
- package/dist/ptable_shared.cjs.map +1 -0
- package/dist/ptable_shared.d.ts +7 -0
- package/dist/ptable_shared.d.ts.map +1 -0
- package/dist/ptable_shared.js +8 -0
- package/dist/ptable_shared.js.map +1 -0
- package/package.json +54 -0
- package/src/data_info_helpers.ts +26 -0
- package/src/driver_decl.ts +82 -0
- package/src/driver_double.test.ts +135 -0
- package/src/driver_double.ts +134 -0
- package/src/driver_impl.ts +535 -0
- package/src/index.ts +4 -0
- package/src/logging.ts +5 -0
- package/src/pframe_pool.ts +257 -0
- package/src/ptable_cache_per_frame.ts +86 -0
- package/src/ptable_cache_plain.ts +67 -0
- package/src/ptable_def_pool.ts +50 -0
- package/src/ptable_pool.ts +187 -0
- package/src/ptable_shared.ts +11 -0
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import {
|
|
2
|
+
assertNever,
|
|
3
|
+
bigintReplacer,
|
|
4
|
+
canonicalizeJson,
|
|
5
|
+
ensureError,
|
|
6
|
+
mapPObjectData,
|
|
7
|
+
PFrameDriverError,
|
|
8
|
+
type JsonSerializable,
|
|
9
|
+
type PColumn,
|
|
10
|
+
type PFrameHandle,
|
|
11
|
+
} from '@platforma-sdk/model';
|
|
12
|
+
import { PFrameInternal } from '@milaboratories/pl-model-middle-layer';
|
|
13
|
+
import {
|
|
14
|
+
hashJson,
|
|
15
|
+
RefCountPoolBase,
|
|
16
|
+
type PoolEntry,
|
|
17
|
+
} from '@milaboratories/ts-helpers';
|
|
18
|
+
import { PFrameFactory } from '@milaboratories/pframes-rs-node';
|
|
19
|
+
import { mapValues } from 'es-toolkit';
|
|
20
|
+
import { logPFrames } from './logging';
|
|
21
|
+
|
|
22
|
+
export interface LocalBlobProvider<TreeEntry extends JsonSerializable> {
|
|
23
|
+
acquire(params: TreeEntry): PoolEntry<PFrameInternal.PFrameBlobId>;
|
|
24
|
+
makeDataSource(signal: AbortSignal): Omit<PFrameInternal.PFrameDataSourceV2, 'parquetServer'>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface RemoteBlobProvider<TreeEntry extends JsonSerializable> {
|
|
28
|
+
acquire(params: TreeEntry): PoolEntry<PFrameInternal.PFrameBlobId>;
|
|
29
|
+
httpServerInfo(): PFrameInternal.HttpServerInfo;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export class PFrameHolder<TreeEntry extends JsonSerializable> implements Disposable {
|
|
33
|
+
public readonly pFramePromise: Promise<PFrameInternal.PFrameV12>;
|
|
34
|
+
private readonly abortController = new AbortController();
|
|
35
|
+
|
|
36
|
+
private readonly localBlobs: PoolEntry<PFrameInternal.PFrameBlobId>[] = [];
|
|
37
|
+
private readonly remoteBlobs: PoolEntry<PFrameInternal.PFrameBlobId>[] = [];
|
|
38
|
+
|
|
39
|
+
constructor(
|
|
40
|
+
private readonly localBlobProvider: LocalBlobProvider<TreeEntry>,
|
|
41
|
+
private readonly remoteBlobProvider: RemoteBlobProvider<TreeEntry>,
|
|
42
|
+
logger: PFrameInternal.Logger,
|
|
43
|
+
private readonly spillPath: string,
|
|
44
|
+
columns: PColumn<PFrameInternal.DataInfo<TreeEntry>>[],
|
|
45
|
+
) {
|
|
46
|
+
const makeLocalBlobId = (blob: TreeEntry): PFrameInternal.PFrameBlobId => {
|
|
47
|
+
const localBlob = this.localBlobProvider.acquire(blob);
|
|
48
|
+
this.localBlobs.push(localBlob);
|
|
49
|
+
return localBlob.key;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const makeRemoteBlobId = (blob: TreeEntry): PFrameInternal.PFrameBlobId => {
|
|
53
|
+
const remoteBlob = this.remoteBlobProvider.acquire(blob);
|
|
54
|
+
this.remoteBlobs.push(remoteBlob);
|
|
55
|
+
return `${remoteBlob.key}${PFrameInternal.ParquetExtension}`;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const mapColumnData = (
|
|
59
|
+
data: PFrameInternal.DataInfo<TreeEntry>,
|
|
60
|
+
): PFrameInternal.DataInfo<PFrameInternal.PFrameBlobId> => {
|
|
61
|
+
switch (data.type) {
|
|
62
|
+
case 'Json':
|
|
63
|
+
return { ...data };
|
|
64
|
+
case 'JsonPartitioned':
|
|
65
|
+
return {
|
|
66
|
+
...data,
|
|
67
|
+
parts: mapValues(data.parts, makeLocalBlobId),
|
|
68
|
+
};
|
|
69
|
+
case 'BinaryPartitioned':
|
|
70
|
+
return {
|
|
71
|
+
...data,
|
|
72
|
+
parts: mapValues(data.parts, (v) => ({
|
|
73
|
+
index: makeLocalBlobId(v.index),
|
|
74
|
+
values: makeLocalBlobId(v.values),
|
|
75
|
+
})),
|
|
76
|
+
};
|
|
77
|
+
case 'ParquetPartitioned':
|
|
78
|
+
return {
|
|
79
|
+
...data,
|
|
80
|
+
parts: mapValues(data.parts, (v) => ({
|
|
81
|
+
...v,
|
|
82
|
+
data: makeRemoteBlobId(v.data),
|
|
83
|
+
})),
|
|
84
|
+
};
|
|
85
|
+
default:
|
|
86
|
+
assertNever(data);
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const jsonifiedColumns = columns.map((column) => ({
|
|
91
|
+
...column,
|
|
92
|
+
data: mapColumnData(column.data),
|
|
93
|
+
}));
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
const pFrame = PFrameFactory.createPFrame({ spillPath: this.spillPath, logger });
|
|
97
|
+
pFrame.setDataSource({
|
|
98
|
+
...this.localBlobProvider.makeDataSource(this.disposeSignal),
|
|
99
|
+
parquetServer: this.remoteBlobProvider.httpServerInfo(),
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const promises: Promise<void>[] = [];
|
|
103
|
+
for (const column of jsonifiedColumns) {
|
|
104
|
+
pFrame.addColumnSpec(column.id, column.spec);
|
|
105
|
+
promises.push(pFrame.setColumnData(column.id, column.data, { signal: this.disposeSignal }));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
this.pFramePromise = Promise.all(promises)
|
|
109
|
+
.then(() => pFrame)
|
|
110
|
+
.catch((err) => {
|
|
111
|
+
this.dispose();
|
|
112
|
+
pFrame.dispose();
|
|
113
|
+
throw new PFrameDriverError(
|
|
114
|
+
`PFrame creation failed asynchronously, `
|
|
115
|
+
+ `columns: ${JSON.stringify(jsonifiedColumns)}, `
|
|
116
|
+
+ `error: ${ensureError(err)}`,
|
|
117
|
+
);
|
|
118
|
+
});
|
|
119
|
+
} catch (err: unknown) {
|
|
120
|
+
throw new PFrameDriverError(
|
|
121
|
+
`PFrame creation failed synchronously, `
|
|
122
|
+
+ `columns: ${JSON.stringify(jsonifiedColumns)}, `
|
|
123
|
+
+ `error: ${ensureError(err)}`,
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
public get disposeSignal(): AbortSignal {
|
|
129
|
+
return this.abortController.signal;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
private dispose(): void {
|
|
133
|
+
this.abortController.abort();
|
|
134
|
+
this.localBlobs.forEach((entry) => entry.unref());
|
|
135
|
+
this.remoteBlobs.forEach((entry) => entry.unref());
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
[Symbol.dispose](): void {
|
|
139
|
+
this.dispose();
|
|
140
|
+
void this.pFramePromise
|
|
141
|
+
.then((pFrame) => pFrame.dispose())
|
|
142
|
+
.catch(() => { /* mute error */ });
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export class PFramePool<TreeEntry extends JsonSerializable>
|
|
147
|
+
extends RefCountPoolBase<
|
|
148
|
+
PColumn<PFrameInternal.DataInfo<TreeEntry>>[],
|
|
149
|
+
PFrameHandle,
|
|
150
|
+
PFrameHolder<TreeEntry>> {
|
|
151
|
+
constructor(
|
|
152
|
+
private readonly localBlobProvider: LocalBlobProvider<TreeEntry>,
|
|
153
|
+
private readonly remoteBlobProvider: RemoteBlobProvider<TreeEntry>,
|
|
154
|
+
private readonly logger: PFrameInternal.Logger,
|
|
155
|
+
private readonly spillPath: string,
|
|
156
|
+
) {
|
|
157
|
+
super();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
protected calculateParamsKey(params: PColumn<PFrameInternal.DataInfo<TreeEntry>>[]): PFrameHandle {
|
|
161
|
+
return stableKeyFromPFrameData(params);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
protected createNewResource(
|
|
165
|
+
params: PColumn<PFrameInternal.DataInfo<TreeEntry>>[],
|
|
166
|
+
key: PFrameHandle,
|
|
167
|
+
): PFrameHolder<TreeEntry> {
|
|
168
|
+
if (logPFrames()) {
|
|
169
|
+
this.logger('info',
|
|
170
|
+
`PFrame creation (pFrameHandle = ${key}): `
|
|
171
|
+
+ `${JSON.stringify(params, bigintReplacer)}`,
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
return new PFrameHolder(
|
|
175
|
+
this.localBlobProvider,
|
|
176
|
+
this.remoteBlobProvider,
|
|
177
|
+
this.logger,
|
|
178
|
+
this.spillPath,
|
|
179
|
+
params,
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
public getByKey(key: PFrameHandle): PFrameHolder<TreeEntry> {
|
|
184
|
+
const resource = super.tryGetByKey(key);
|
|
185
|
+
if (!resource) throw new PFrameDriverError(`PFrame not found, handle = ${key}`);
|
|
186
|
+
return resource;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function stableKeyFromPFrameData<TreeEntry extends JsonSerializable>(
|
|
191
|
+
data: PColumn<PFrameInternal.DataInfo<TreeEntry>>[],
|
|
192
|
+
): PFrameHandle {
|
|
193
|
+
const orderedData = [...data].map((column) =>
|
|
194
|
+
mapPObjectData(column, (r) => {
|
|
195
|
+
let result: {
|
|
196
|
+
type: string;
|
|
197
|
+
keyLength: number;
|
|
198
|
+
payload: {
|
|
199
|
+
key: string;
|
|
200
|
+
value: null | number | string | [string, string];
|
|
201
|
+
}[];
|
|
202
|
+
};
|
|
203
|
+
const type = r.type;
|
|
204
|
+
switch (type) {
|
|
205
|
+
case 'Json':
|
|
206
|
+
result = {
|
|
207
|
+
type: r.type,
|
|
208
|
+
keyLength: r.keyLength,
|
|
209
|
+
payload: Object.entries(r.data).map(([part, value]) => ({
|
|
210
|
+
key: part,
|
|
211
|
+
value,
|
|
212
|
+
})),
|
|
213
|
+
};
|
|
214
|
+
break;
|
|
215
|
+
case 'JsonPartitioned':
|
|
216
|
+
result = {
|
|
217
|
+
type: r.type,
|
|
218
|
+
keyLength: r.partitionKeyLength,
|
|
219
|
+
payload: Object.entries(r.parts).map(([part, info]) => ({
|
|
220
|
+
key: part,
|
|
221
|
+
value: canonicalizeJson(info),
|
|
222
|
+
})),
|
|
223
|
+
};
|
|
224
|
+
break;
|
|
225
|
+
case 'BinaryPartitioned':
|
|
226
|
+
result = {
|
|
227
|
+
type: r.type,
|
|
228
|
+
keyLength: r.partitionKeyLength,
|
|
229
|
+
payload: Object.entries(r.parts).map(([part, info]) => ({
|
|
230
|
+
key: part,
|
|
231
|
+
value: [canonicalizeJson(info.index), canonicalizeJson(info.values)] as const,
|
|
232
|
+
})),
|
|
233
|
+
};
|
|
234
|
+
break;
|
|
235
|
+
case 'ParquetPartitioned':
|
|
236
|
+
result = {
|
|
237
|
+
type: r.type,
|
|
238
|
+
keyLength: r.partitionKeyLength,
|
|
239
|
+
payload: Object.entries(r.parts).map(([part, info]) => ({
|
|
240
|
+
key: part,
|
|
241
|
+
value: info.dataDigest || [
|
|
242
|
+
canonicalizeJson(info.data),
|
|
243
|
+
JSON.stringify({ axes: info.axes, column: info.column }),
|
|
244
|
+
] as const,
|
|
245
|
+
})),
|
|
246
|
+
};
|
|
247
|
+
break;
|
|
248
|
+
default:
|
|
249
|
+
throw new PFrameDriverError(`unsupported resource type: ${JSON.stringify(type satisfies never)}`);
|
|
250
|
+
}
|
|
251
|
+
result.payload.sort((lhs, rhs) => lhs.key < rhs.key ? -1 : 1);
|
|
252
|
+
return result;
|
|
253
|
+
}),
|
|
254
|
+
);
|
|
255
|
+
orderedData.sort((lhs, rhs) => lhs.id < rhs.id ? -1 : 1);
|
|
256
|
+
return hashJson(orderedData) as string as PFrameHandle;
|
|
257
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { PFrameHandle, PTableHandle } from '@platforma-sdk/model';
|
|
2
|
+
import type { PFrameInternal } from '@milaboratories/pl-model-middle-layer';
|
|
3
|
+
import type { PoolEntry } from '@milaboratories/ts-helpers';
|
|
4
|
+
import { LRUCache } from 'lru-cache';
|
|
5
|
+
import { logPFrames } from './logging';
|
|
6
|
+
import type { PTableHolder } from './ptable_pool';
|
|
7
|
+
|
|
8
|
+
export type PTableCachePerFrameOps = {
|
|
9
|
+
/** Maximum number of `calculateTableData` results cached for each PFrame */
|
|
10
|
+
pFrameCacheMaxCount: number;
|
|
11
|
+
/**
|
|
12
|
+
* Maximum size of `calculateTableData` results cached for PFrames overall.
|
|
13
|
+
* The limit is soft, as the same table could be materialized with other requests and will not be deleted in such case.
|
|
14
|
+
* Also each table has predeccessors, overlapping predecessors will be counted twice, so the effective limit is smaller.
|
|
15
|
+
*/
|
|
16
|
+
pFramesCacheMaxSize: number;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const PTableCachePerFrameOpsDefaults: PTableCachePerFrameOps = {
|
|
20
|
+
pFrameCacheMaxCount: 18, // SHM trees create 3 PTables per graphic, we want to cache 6 graphics per PFrame
|
|
21
|
+
pFramesCacheMaxSize: 8 * 1024 * 1024 * 1024, // 8 GB, same as blob driver cache (must be at least 2GB)
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export class PTableCachePerFrame {
|
|
25
|
+
private readonly perFrame = new Map<PFrameHandle, LRUCache<PTableHandle, PoolEntry<PTableHandle, PTableHolder>>>();
|
|
26
|
+
private readonly global: LRUCache<PTableHandle, PoolEntry<PTableHandle, PTableHolder>>;
|
|
27
|
+
private readonly disposeListeners = new Set<PTableHandle>();
|
|
28
|
+
|
|
29
|
+
constructor(
|
|
30
|
+
private readonly logger: PFrameInternal.Logger,
|
|
31
|
+
private readonly ops: PTableCachePerFrameOps,
|
|
32
|
+
) {
|
|
33
|
+
this.global = new LRUCache<PTableHandle, PoolEntry<PTableHandle, PTableHolder>>({
|
|
34
|
+
maxSize: this.ops.pFramesCacheMaxSize,
|
|
35
|
+
dispose: (resource, key, reason) => {
|
|
36
|
+
if (reason === 'evict') {
|
|
37
|
+
this.perFrame.get(resource.resource.pFrame)?.delete(key);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (this.perFrame.get(resource.resource.pFrame)?.size === 0) {
|
|
41
|
+
this.perFrame.delete(resource.resource.pFrame);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
resource.unref();
|
|
45
|
+
if (logPFrames()) {
|
|
46
|
+
logger('info', `calculateTableData cache - removed PTable ${key} (reason: ${reason})`);
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
public cache(resource: PoolEntry<PTableHandle, PTableHolder>, size: number): void {
|
|
53
|
+
const key = resource.key;
|
|
54
|
+
if (logPFrames()) {
|
|
55
|
+
this.logger('info', `calculateTableData cache - added PTable ${key} with size ${size}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
this.global.set(key, resource, { size: Math.max(size, 1) }); // 1 is minimum size to avoid cache evictions
|
|
59
|
+
|
|
60
|
+
let perFrame = this.perFrame.get(resource.resource.pFrame);
|
|
61
|
+
if (!perFrame) {
|
|
62
|
+
perFrame = new LRUCache<PTableHandle, PoolEntry<PTableHandle, PTableHolder>>({
|
|
63
|
+
max: this.ops.pFrameCacheMaxCount,
|
|
64
|
+
dispose: (_resource, key, reason) => {
|
|
65
|
+
if (reason === 'evict') {
|
|
66
|
+
this.global.delete(key);
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
this.perFrame.set(resource.resource.pFrame, perFrame);
|
|
71
|
+
}
|
|
72
|
+
perFrame.set(key, resource);
|
|
73
|
+
|
|
74
|
+
if (!this.disposeListeners.has(key)) {
|
|
75
|
+
const disposeListener = () => {
|
|
76
|
+
this.perFrame.get(resource.resource.pFrame)?.delete(key);
|
|
77
|
+
this.global.delete(key);
|
|
78
|
+
|
|
79
|
+
this.disposeListeners.delete(key);
|
|
80
|
+
resource.resource.disposeSignal.removeEventListener('abort', disposeListener);
|
|
81
|
+
};
|
|
82
|
+
this.disposeListeners.add(key);
|
|
83
|
+
resource.resource.disposeSignal.addEventListener('abort', disposeListener);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { PTableHandle } from '@platforma-sdk/model';
|
|
2
|
+
import type { PFrameInternal } from '@milaboratories/pl-model-middle-layer';
|
|
3
|
+
import type { PoolEntry } from '@milaboratories/ts-helpers';
|
|
4
|
+
import { LRUCache } from 'lru-cache';
|
|
5
|
+
import { logPFrames } from './logging';
|
|
6
|
+
import type { PTableHolder } from './ptable_pool';
|
|
7
|
+
|
|
8
|
+
export type PTableCachePlainOps = {
|
|
9
|
+
/**
|
|
10
|
+
* Maximum size of `createPTable` results cached on disk.
|
|
11
|
+
* The limit is soft, as the same table could be materialized with other requests and will not be deleted in such case.
|
|
12
|
+
* Also each table has predeccessors, overlapping predecessors will be counted twice, so the effective limit is smaller.
|
|
13
|
+
*/
|
|
14
|
+
pTablesCacheMaxSize: number;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const PTableCachePlainOpsDefaults: PTableCachePlainOps = {
|
|
18
|
+
pTablesCacheMaxSize: 32 * 1024 * 1024 * 1024, // 32 GB (must be at least 8GB)
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export class PTableCachePlain {
|
|
22
|
+
private readonly global: LRUCache<PTableHandle, PoolEntry<PTableHandle, PTableHolder>>;
|
|
23
|
+
private readonly disposeListeners = new Set<PTableHandle>();
|
|
24
|
+
|
|
25
|
+
constructor(
|
|
26
|
+
private readonly logger: PFrameInternal.Logger,
|
|
27
|
+
ops: PTableCachePlainOps,
|
|
28
|
+
) {
|
|
29
|
+
this.global = new LRUCache<PTableHandle, PoolEntry<PTableHandle, PTableHolder>>({
|
|
30
|
+
maxSize: ops.pTablesCacheMaxSize,
|
|
31
|
+
dispose: (resource, key, reason) => {
|
|
32
|
+
resource.unref();
|
|
33
|
+
if (logPFrames()) {
|
|
34
|
+
logger('info', `createPTable cache - removed PTable ${key} (reason: ${reason})`);
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
public cache(resource: PoolEntry<PTableHandle, PTableHolder>, size: number, defDisposeSignal: AbortSignal): void {
|
|
41
|
+
const key = resource.key;
|
|
42
|
+
if (logPFrames()) {
|
|
43
|
+
this.logger('info', `createPTable cache - added PTable ${key} with size ${size}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const status: LRUCache.Status<PoolEntry<PTableHandle, PTableHolder>> = {};
|
|
47
|
+
this.global.set(key, resource, { size: Math.max(size, 1), status }); // 1 is minimum size to avoid cache evictions
|
|
48
|
+
|
|
49
|
+
if (status.maxEntrySizeExceeded) {
|
|
50
|
+
resource.unref();
|
|
51
|
+
if (logPFrames()) {
|
|
52
|
+
this.logger('info', `createPTable cache - removed PTable ${key} (maxEntrySizeExceeded)`);
|
|
53
|
+
}
|
|
54
|
+
} else {
|
|
55
|
+
if (!this.disposeListeners.has(key)) {
|
|
56
|
+
const disposeListener = () => {
|
|
57
|
+
this.global.delete(key);
|
|
58
|
+
|
|
59
|
+
this.disposeListeners.delete(key);
|
|
60
|
+
defDisposeSignal.removeEventListener('abort', disposeListener);
|
|
61
|
+
};
|
|
62
|
+
this.disposeListeners.add(key);
|
|
63
|
+
defDisposeSignal.addEventListener('abort', disposeListener);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { PFrameDriverError, type PTableHandle } from '@platforma-sdk/model';
|
|
2
|
+
import type { PFrameInternal } from '@milaboratories/pl-model-middle-layer';
|
|
3
|
+
import { RefCountPoolBase } from '@milaboratories/ts-helpers';
|
|
4
|
+
import { logPFrames } from './logging';
|
|
5
|
+
import { stableKeyFromFullPTableDef, type FullPTableDef } from './ptable_shared';
|
|
6
|
+
|
|
7
|
+
export class PTableDefHolder implements Disposable {
|
|
8
|
+
private readonly abortController = new AbortController();
|
|
9
|
+
|
|
10
|
+
constructor(
|
|
11
|
+
public readonly def: FullPTableDef,
|
|
12
|
+
private readonly pTableHandle: PTableHandle,
|
|
13
|
+
private readonly logger: PFrameInternal.Logger,
|
|
14
|
+
) {
|
|
15
|
+
if (logPFrames()) {
|
|
16
|
+
this.logger('info', `PTable definition saved (pTableHandle = ${this.pTableHandle})`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
public get disposeSignal(): AbortSignal {
|
|
21
|
+
return this.abortController.signal;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
[Symbol.dispose](): void {
|
|
25
|
+
this.abortController.abort();
|
|
26
|
+
if (logPFrames()) {
|
|
27
|
+
this.logger('info', `PTable definition disposed (pTableHandle = ${this.pTableHandle})`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export class PTableDefPool extends RefCountPoolBase<FullPTableDef, PTableHandle, PTableDefHolder> {
|
|
33
|
+
constructor(private readonly logger: PFrameInternal.Logger) {
|
|
34
|
+
super();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
protected calculateParamsKey(params: FullPTableDef): PTableHandle {
|
|
38
|
+
return stableKeyFromFullPTableDef(params);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
protected createNewResource(params: FullPTableDef, key: PTableHandle): PTableDefHolder {
|
|
42
|
+
return new PTableDefHolder(params, key, this.logger);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
public getByKey(key: PTableHandle): PTableDefHolder {
|
|
46
|
+
const resource = super.tryGetByKey(key);
|
|
47
|
+
if (!resource) throw new PFrameDriverError(`PTable definition not found, handle = ${key}`);
|
|
48
|
+
return resource;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import {
|
|
2
|
+
assertNever,
|
|
3
|
+
bigintReplacer,
|
|
4
|
+
PFrameDriverError,
|
|
5
|
+
type PFrameHandle,
|
|
6
|
+
type PTableHandle,
|
|
7
|
+
type JoinEntry,
|
|
8
|
+
type JsonSerializable,
|
|
9
|
+
type PColumnValue,
|
|
10
|
+
type PObjectId,
|
|
11
|
+
} from '@platforma-sdk/model';
|
|
12
|
+
import type { PFrameInternal } from '@milaboratories/pl-model-middle-layer';
|
|
13
|
+
import { RefCountPoolBase, type PoolEntry } from '@milaboratories/ts-helpers';
|
|
14
|
+
import { logPFrames } from './logging';
|
|
15
|
+
import type { PFramePool } from './pframe_pool';
|
|
16
|
+
import { stableKeyFromFullPTableDef, type FullPTableDef } from './ptable_shared';
|
|
17
|
+
import type { PTableDefPool } from './ptable_def_pool';
|
|
18
|
+
|
|
19
|
+
export class PTableHolder implements Disposable {
|
|
20
|
+
private readonly abortController = new AbortController();
|
|
21
|
+
private readonly combinedDisposeSignal: AbortSignal;
|
|
22
|
+
|
|
23
|
+
constructor(
|
|
24
|
+
public readonly pFrame: PFrameHandle,
|
|
25
|
+
pFrameDisposeSignal: AbortSignal,
|
|
26
|
+
public readonly pTablePromise: Promise<PFrameInternal.PTableV7>,
|
|
27
|
+
private readonly predecessor?: PoolEntry<PTableHandle, PTableHolder>,
|
|
28
|
+
) {
|
|
29
|
+
this.combinedDisposeSignal = AbortSignal.any([pFrameDisposeSignal, this.abortController.signal]);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
public get disposeSignal(): AbortSignal {
|
|
33
|
+
return this.combinedDisposeSignal;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
[Symbol.dispose](): void {
|
|
37
|
+
this.abortController.abort();
|
|
38
|
+
this.predecessor?.unref();
|
|
39
|
+
void this.pTablePromise
|
|
40
|
+
.then((pTable) => pTable.dispose())
|
|
41
|
+
.catch(() => { /* mute error */ });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export class PTablePool<TreeEntry extends JsonSerializable>
|
|
46
|
+
extends RefCountPoolBase<FullPTableDef, PTableHandle, PTableHolder> {
|
|
47
|
+
constructor(
|
|
48
|
+
private readonly pFrames: PFramePool<TreeEntry>,
|
|
49
|
+
private readonly pTableDefs: PTableDefPool,
|
|
50
|
+
private readonly logger: PFrameInternal.Logger,
|
|
51
|
+
) {
|
|
52
|
+
super();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
protected calculateParamsKey(params: FullPTableDef): PTableHandle {
|
|
56
|
+
return stableKeyFromFullPTableDef(params);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
protected createNewResource(params: FullPTableDef, key: PTableHandle): PTableHolder {
|
|
60
|
+
if (logPFrames()) {
|
|
61
|
+
this.logger('info',
|
|
62
|
+
`PTable creation (pTableHandle = ${key}): `
|
|
63
|
+
+ `${JSON.stringify(params, bigintReplacer)}`,
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const handle = params.pFrameHandle;
|
|
68
|
+
const { pFramePromise, disposeSignal } = this.pFrames.getByKey(handle);
|
|
69
|
+
|
|
70
|
+
const defDisposeSignal = this.pTableDefs.tryGetByKey(key)?.disposeSignal;
|
|
71
|
+
const combinedSignal = AbortSignal.any([disposeSignal, defDisposeSignal].filter((s) => !!s));
|
|
72
|
+
|
|
73
|
+
// 3. Sort
|
|
74
|
+
if (params.def.sorting.length > 0) {
|
|
75
|
+
const predecessor = this.acquire({
|
|
76
|
+
...params,
|
|
77
|
+
def: {
|
|
78
|
+
...params.def,
|
|
79
|
+
sorting: [],
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
const { resource: { pTablePromise } } = predecessor;
|
|
83
|
+
const sortedTable = pTablePromise.then((pTable) => pTable.sort(params.def.sorting));
|
|
84
|
+
return new PTableHolder(handle, combinedSignal, sortedTable, predecessor);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// 2. Filter (except the case with artificial columns where cartesian creates too many rows)
|
|
88
|
+
if (!hasArtificialColumns(params.def.src) && params.def.filters.length > 0) {
|
|
89
|
+
const predecessor = this.acquire({
|
|
90
|
+
...params,
|
|
91
|
+
def: {
|
|
92
|
+
...params.def,
|
|
93
|
+
filters: [],
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
const { resource: { pTablePromise } } = predecessor;
|
|
97
|
+
const filteredTable = pTablePromise.then((pTable) => pTable.filter(params.def.filters));
|
|
98
|
+
return new PTableHolder(handle, combinedSignal, filteredTable, predecessor);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// 1. Join
|
|
102
|
+
const table = pFramePromise.then((pFrame) => pFrame.createTable({
|
|
103
|
+
src: joinEntryToInternal(params.def.src),
|
|
104
|
+
// `params.def.filters` would be non-empty only when join has artificial columns
|
|
105
|
+
filters: [...params.def.partitionFilters, ...params.def.filters],
|
|
106
|
+
}));
|
|
107
|
+
return new PTableHolder(handle, combinedSignal, table);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
public getByKey(key: PTableHandle): PTableHolder {
|
|
111
|
+
const resource = super.tryGetByKey(key);
|
|
112
|
+
if (!resource) throw new PFrameDriverError(`PTable not found, handle = ${key}`);
|
|
113
|
+
return resource;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function hasArtificialColumns<T>(entry: JoinEntry<T>): boolean {
|
|
118
|
+
switch (entry.type) {
|
|
119
|
+
case 'column':
|
|
120
|
+
case 'slicedColumn':
|
|
121
|
+
case 'inlineColumn':
|
|
122
|
+
return false;
|
|
123
|
+
case 'artificialColumn':
|
|
124
|
+
return true;
|
|
125
|
+
case 'full':
|
|
126
|
+
case 'inner':
|
|
127
|
+
return entry.entries.some(hasArtificialColumns);
|
|
128
|
+
case 'outer':
|
|
129
|
+
return hasArtificialColumns(entry.primary) || entry.secondary.some(hasArtificialColumns);
|
|
130
|
+
default:
|
|
131
|
+
assertNever(entry);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function joinEntryToInternal(entry: JoinEntry<PObjectId>): PFrameInternal.JoinEntryV4 {
|
|
136
|
+
const type = entry.type;
|
|
137
|
+
switch (type) {
|
|
138
|
+
case 'column':
|
|
139
|
+
return {
|
|
140
|
+
type: 'column',
|
|
141
|
+
columnId: entry.column,
|
|
142
|
+
};
|
|
143
|
+
case 'slicedColumn':
|
|
144
|
+
return {
|
|
145
|
+
type: 'slicedColumn',
|
|
146
|
+
columnId: entry.column,
|
|
147
|
+
newId: entry.newId,
|
|
148
|
+
axisFilters: entry.axisFilters,
|
|
149
|
+
};
|
|
150
|
+
case 'artificialColumn':
|
|
151
|
+
return {
|
|
152
|
+
type: 'artificialColumn',
|
|
153
|
+
columnId: entry.column,
|
|
154
|
+
newId: entry.newId,
|
|
155
|
+
axesIndices: entry.axesIndices,
|
|
156
|
+
};
|
|
157
|
+
case 'inlineColumn':
|
|
158
|
+
return {
|
|
159
|
+
type: 'inlineColumn',
|
|
160
|
+
newId: entry.column.id,
|
|
161
|
+
spec: entry.column.spec,
|
|
162
|
+
dataInfo: {
|
|
163
|
+
type: 'Json',
|
|
164
|
+
keyLength: entry.column.spec.axesSpec.length,
|
|
165
|
+
data: entry.column.data.reduce((acc, row) => {
|
|
166
|
+
acc[JSON.stringify(row.key)] = row.val;
|
|
167
|
+
return acc;
|
|
168
|
+
}, {} as Record<string, PColumnValue>),
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
case 'inner':
|
|
172
|
+
case 'full':
|
|
173
|
+
return {
|
|
174
|
+
type: entry.type,
|
|
175
|
+
entries: entry.entries.map((col) => joinEntryToInternal(col)),
|
|
176
|
+
};
|
|
177
|
+
case 'outer':
|
|
178
|
+
return {
|
|
179
|
+
type: 'outer',
|
|
180
|
+
primary: joinEntryToInternal(entry.primary),
|
|
181
|
+
secondary: entry.secondary.map((col) => joinEntryToInternal(col)),
|
|
182
|
+
};
|
|
183
|
+
default:
|
|
184
|
+
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
|
185
|
+
throw new PFrameDriverError(`unsupported PFrame join entry type: ${type satisfies never}`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { PObjectId, PFrameHandle, PTableDef, PTableHandle } from '@platforma-sdk/model';
|
|
2
|
+
import { hashJson } from '@milaboratories/ts-helpers';
|
|
3
|
+
|
|
4
|
+
export type FullPTableDef = {
|
|
5
|
+
pFrameHandle: PFrameHandle;
|
|
6
|
+
def: PTableDef<PObjectId>;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export function stableKeyFromFullPTableDef(data: FullPTableDef): PTableHandle {
|
|
10
|
+
return hashJson(data) as string as PTableHandle;
|
|
11
|
+
}
|