@milaboratories/pl-tree 1.3.17 → 1.3.18

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/src/sync.ts CHANGED
@@ -1,12 +1,14 @@
1
1
  import {
2
2
  FieldData,
3
3
  isNullResourceId,
4
+ NullResourceId,
4
5
  OptionalResourceId,
5
6
  PlTransaction,
6
7
  ResourceId
7
8
  } from '@milaboratories/pl-client';
8
9
  import Denque from 'denque';
9
10
  import { ExtendedResourceData, PlTreeState } from './state';
11
+ import { msToHumanReadable } from '@milaboratories/ts-helpers';
10
12
 
11
13
  /** Applied to list of fields in resource data. */
12
14
  export type PruningFunction = (resource: ExtendedResourceData) => FieldData[];
@@ -49,13 +51,59 @@ export function constructTreeLoadingRequest(
49
51
  return { seedResources, finalResources, pruningFunction };
50
52
  }
51
53
 
54
+ export type TreeLoadingStat = {
55
+ requests: number;
56
+ roundtrips: number;
57
+ retrievedResources: number;
58
+ retrievedFields: number;
59
+ retrievedKeyValues: number;
60
+ retrievedResourceDataBytes: number;
61
+ prunnedFields: number;
62
+ finalResourcesSkipped: number;
63
+ millisSpent: number;
64
+ };
65
+
66
+ export function initialTreeLoadingStat(): TreeLoadingStat {
67
+ return {
68
+ requests: 0,
69
+ roundtrips: 0,
70
+ retrievedResources: 0,
71
+ retrievedFields: 0,
72
+ retrievedKeyValues: 0,
73
+ retrievedResourceDataBytes: 0,
74
+ prunnedFields: 0,
75
+ finalResourcesSkipped: 0,
76
+ millisSpent: 0
77
+ };
78
+ }
79
+
80
+ export function formatTreeLoadingStat(stat: TreeLoadingStat): string {
81
+ let result = `Requests: ${stat.requests}\n`;
82
+ result += `Total time: ${msToHumanReadable(stat.millisSpent)}\n`;
83
+ result += `Roundtrips: ${stat.roundtrips}\n`;
84
+ result += `Resources: ${stat.retrievedResources}\n`;
85
+ result += `Fields: ${stat.retrievedFields}\n`;
86
+ result += `KV: ${stat.retrievedKeyValues}\n`;
87
+ result += `Data Bytes: ${stat.retrievedResourceDataBytes}\n`;
88
+ result += `Pruned fields: ${stat.prunnedFields}\n`;
89
+ result += `Final resources skipped: ${stat.finalResourcesSkipped}`;
90
+ return result;
91
+ }
92
+
52
93
  /** Given the transaction (preferably read-only) and loading request, executes
53
94
  * the tree traversal algorithm, and collects fresh states of resources
54
95
  * to update the tree state. */
55
96
  export async function loadTreeState(
56
97
  tx: PlTransaction,
57
- loadingRequest: TreeLoadingRequest
98
+ loadingRequest: TreeLoadingRequest,
99
+ stats?: TreeLoadingStat
58
100
  ): Promise<ExtendedResourceData[]> {
101
+ // saving start timestamp to add time spent in this function to the stats at the end of the method
102
+ const startTimestamp = Date.now();
103
+
104
+ // countind the request
105
+ if (stats) stats.requests++;
106
+
59
107
  const { seedResources, finalResources, pruningFunction } = loadingRequest;
60
108
 
61
109
  // Main idea of using a queue here is that responses will arrive in the same order as they were
@@ -65,10 +113,20 @@ export async function loadTreeState(
65
113
 
66
114
  const pending = new Denque<Promise<ExtendedResourceData | undefined>>();
67
115
 
116
+ // vars to calculate number of roundtrips for stats
117
+ let roundtripToggle: boolean = true;
118
+ let numberOfRoundtrips = 0;
119
+
68
120
  // tracking resources we already requested
69
121
  const requested = new Set<ResourceId>();
70
122
  const requestState = (rid: OptionalResourceId) => {
71
- if (isNullResourceId(rid) || requested.has(rid) || finalResources.has(rid)) return;
123
+ if (isNullResourceId(rid) || requested.has(rid)) return;
124
+
125
+ // separate check to collect stats
126
+ if (finalResources.has(rid)) {
127
+ if (stats) stats.finalResourcesSkipped++;
128
+ return;
129
+ }
72
130
 
73
131
  // adding the id, so we will not request it's state again if somebody else
74
132
  // references the same resource
@@ -78,11 +136,20 @@ export async function loadTreeState(
78
136
  const resourceData = tx.getResourceDataIfExists(rid, true);
79
137
  const kvData = tx.listKeyValuesIfResourceExists(rid);
80
138
 
139
+ // counting roundrip (begin)
140
+ const addRT = roundtripToggle;
141
+ if (roundtripToggle) roundtripToggle = false;
142
+
81
143
  // pushing combined promise
82
144
  pending.push(
83
145
  (async () => {
84
- const resource = await resourceData;
85
- const kv = await kvData;
146
+ const [resource, kv] = await Promise.all([resourceData, kvData]);
147
+
148
+ // counting roundrip, actually incrementing counter and returning toggle back, so the next request can acquire it
149
+ if (addRT) {
150
+ numberOfRoundtrips++;
151
+ roundtripToggle = true;
152
+ }
86
153
 
87
154
  if (resource === undefined) return undefined;
88
155
 
@@ -110,9 +177,13 @@ export async function loadTreeState(
110
177
  // ignoring resources that were not found (this may happen for seed resource ids)
111
178
  continue;
112
179
 
113
- // apply field pruning, if requested
114
- if (pruningFunction !== undefined)
115
- nextResource = { ...nextResource, fields: pruningFunction(nextResource) };
180
+ if (pruningFunction !== undefined) {
181
+ // apply field pruning, if requested
182
+ const fieldsAfterPruning = pruningFunction(nextResource);
183
+ // collecting stats
184
+ if (stats) stats.prunnedFields += nextResource.fields.length - fieldsAfterPruning.length;
185
+ nextResource = { ...nextResource, fields: fieldsAfterPruning };
186
+ }
116
187
 
117
188
  // continue traversal over the referenced resource
118
189
  requestState(nextResource.error);
@@ -121,9 +192,23 @@ export async function loadTreeState(
121
192
  requestState(field.error);
122
193
  }
123
194
 
195
+ // collecting stats
196
+ if (stats) {
197
+ stats.retrievedResources++;
198
+ stats.retrievedFields += nextResource.fields.length;
199
+ stats.retrievedKeyValues += nextResource.kv.length;
200
+ stats.retrievedResourceDataBytes += nextResource.data?.length ?? 0;
201
+ }
202
+
124
203
  // aggregating the state
125
204
  result.push(nextResource);
126
205
  }
127
206
 
207
+ // adding the time we spent in this method to stats
208
+ if (stats) {
209
+ stats.millisSpent += Date.now() - startTimestamp;
210
+ stats.roundtrips += numberOfRoundtrips;
211
+ }
212
+
128
213
  return result;
129
214
  }
@@ -1,7 +1,9 @@
1
+ import { test, expect } from '@jest/globals';
1
2
  import { field, TestHelpers } from '@milaboratories/pl-client';
2
3
  import { TestStructuralResourceType1 } from './test_utils';
3
4
  import { Computable } from '@milaboratories/computable';
4
5
  import { SynchronizedTreeState } from './synchronized_tree';
6
+ import { ConsoleLoggerAdapter } from '@milaboratories/ts-helpers';
5
7
 
6
8
  test('simple synchronized tree test', async () => {
7
9
  await TestHelpers.withTempRoot(async (pl) => {
@@ -18,10 +20,16 @@ test('simple synchronized tree test', async () => {
18
20
  { sync: true }
19
21
  );
20
22
 
21
- const treeState = await SynchronizedTreeState.init(pl, r1, {
22
- stopPollingDelay: 10,
23
- pollingInterval: 10
24
- });
23
+ const treeState = await SynchronizedTreeState.init(
24
+ pl,
25
+ r1,
26
+ {
27
+ stopPollingDelay: 10,
28
+ pollingInterval: 10,
29
+ logStat: 'cumulative'
30
+ },
31
+ new ConsoleLoggerAdapter(require('console'))
32
+ );
25
33
 
26
34
  const theComputable = Computable.make((c) =>
27
35
  c.accessor(treeState.entry()).node().traverse('a', 'b')?.getDataAsString()
@@ -113,10 +121,16 @@ test('synchronized tree test with KV', async () => {
113
121
  { sync: true }
114
122
  );
115
123
 
116
- const treeState = await SynchronizedTreeState.init(pl, r1, {
117
- stopPollingDelay: 10,
118
- pollingInterval: 10
119
- });
124
+ const treeState = await SynchronizedTreeState.init(
125
+ pl,
126
+ r1,
127
+ {
128
+ stopPollingDelay: 10,
129
+ pollingInterval: 10,
130
+ logStat: 'cumulative'
131
+ },
132
+ new ConsoleLoggerAdapter(require('console'))
133
+ );
120
134
 
121
135
  const theComputable = Computable.make((c) =>
122
136
  c.accessor(treeState.entry()).node().traverse('a')?.getKeyValueAsString('b', true)
@@ -182,10 +196,16 @@ test('termination test', async () => {
182
196
  { sync: true }
183
197
  );
184
198
 
185
- const treeState = await SynchronizedTreeState.init(pl, r1, {
186
- stopPollingDelay: 10,
187
- pollingInterval: 10
188
- });
199
+ const treeState = await SynchronizedTreeState.init(
200
+ pl,
201
+ r1,
202
+ {
203
+ stopPollingDelay: 10,
204
+ pollingInterval: 10,
205
+ logStat: 'cumulative'
206
+ },
207
+ new ConsoleLoggerAdapter(require('console'))
208
+ );
189
209
 
190
210
  const entry = treeState.entry();
191
211
  const theComputable = Computable.make((c) =>
@@ -2,10 +2,18 @@ import { PollingComputableHooks } from '@milaboratories/computable';
2
2
  import { PlTreeEntry } from './accessors';
3
3
  import { isTimeoutOrCancelError, PlClient, ResourceId } from '@milaboratories/pl-client';
4
4
  import { FinalPredicate, PlTreeState, TreeStateUpdateError } from './state';
5
- import { constructTreeLoadingRequest, loadTreeState, PruningFunction } from './sync';
5
+ import {
6
+ constructTreeLoadingRequest,
7
+ initialTreeLoadingStat,
8
+ loadTreeState,
9
+ PruningFunction,
10
+ TreeLoadingStat
11
+ } from './sync';
6
12
  import * as tp from 'node:timers/promises';
7
13
  import { MiLogger } from '@milaboratories/ts-helpers';
8
14
 
15
+ type StatLoggingMode = 'cumulative' | 'per-request';
16
+
9
17
  export type SynchronizedTreeOps = {
10
18
  finalPredicate?: FinalPredicate;
11
19
  pruning?: PruningFunction;
@@ -14,6 +22,9 @@ export type SynchronizedTreeOps = {
14
22
  pollingInterval: number;
15
23
  /** For how long to continue polling after the last derived value access */
16
24
  stopPollingDelay: number;
25
+
26
+ /** If one of the values, tree will log stats of each polling request */
27
+ logStat?: StatLoggingMode;
17
28
  };
18
29
 
19
30
  type ScheduledRefresh = {
@@ -26,6 +37,7 @@ export class SynchronizedTreeState {
26
37
  private state: PlTreeState;
27
38
  private readonly pollingInterval: number;
28
39
  private readonly pruning?: PruningFunction;
40
+ private readonly logStat?: StatLoggingMode;
29
41
  private readonly hooks: PollingComputableHooks;
30
42
  private readonly abortController = new AbortController();
31
43
 
@@ -35,10 +47,11 @@ export class SynchronizedTreeState {
35
47
  ops: SynchronizedTreeOps,
36
48
  private readonly logger?: MiLogger
37
49
  ) {
38
- const { finalPredicate, pruning, pollingInterval, stopPollingDelay } = ops;
50
+ const { finalPredicate, pruning, pollingInterval, stopPollingDelay, logStat } = ops;
39
51
  this.pruning = pruning;
40
52
  this.pollingInterval = pollingInterval;
41
53
  this.finalPredicate = finalPredicate;
54
+ this.logStat = logStat;
42
55
  this.state = new PlTreeState(root, finalPredicate);
43
56
  this.hooks = new PollingComputableHooks(
44
57
  () => this.startUpdating(),
@@ -91,11 +104,11 @@ export class SynchronizedTreeState {
91
104
  private currentLoop: Promise<void> | undefined = undefined;
92
105
 
93
106
  /** Executed from the main loop, and initialization procedure. */
94
- private async refresh(): Promise<void> {
107
+ private async refresh(stats?: TreeLoadingStat): Promise<void> {
95
108
  if (this.terminated) throw new Error('tree synchronization is terminated');
96
109
  const request = constructTreeLoadingRequest(this.state, this.pruning);
97
110
  const data = await this.pl.withReadTx('ReadingTree', async (tx) => {
98
- return await loadTreeState(tx, request);
111
+ return await loadTreeState(tx, request, stats);
99
112
  });
100
113
  this.state.updateFromResourceData(data, true);
101
114
  }
@@ -104,6 +117,9 @@ export class SynchronizedTreeState {
104
117
  private terminated = false;
105
118
 
106
119
  private async mainLoop() {
120
+ // will hold request stats
121
+ let stat = this.logStat ? initialTreeLoadingStat() : undefined;
122
+
107
123
  while (true) {
108
124
  if (!this.keepRunning) break;
109
125
 
@@ -117,12 +133,21 @@ export class SynchronizedTreeState {
117
133
  }
118
134
 
119
135
  try {
136
+ // resetting stats if we were asked to collect non-cumulative stats
137
+ if (this.logStat === 'per-request') stat = initialTreeLoadingStat();
138
+
120
139
  // actual tree synchronization
121
- await this.refresh();
140
+ await this.refresh(stat);
141
+
142
+ // logging stats if we were asked to
143
+ if (stat && this.logger) this.logger.info(`Tree stat (success): ${JSON.stringify(stat)}`);
122
144
 
123
145
  // notifying that we got new state
124
146
  if (toNotify !== undefined) for (const n of toNotify) n.resolve();
125
147
  } catch (e: any) {
148
+ // logging stats if we were asked to (even if error occured)
149
+ if (stat && this.logger) this.logger.info(`Tree stat (error): ${JSON.stringify(stat)}`);
150
+
126
151
  // notifying that we failed to refresh the state
127
152
  if (toNotify !== undefined) for (const n of toNotify) n.reject(e);
128
153
 
@@ -181,8 +206,13 @@ export class SynchronizedTreeState {
181
206
  await this.currentLoop;
182
207
  }
183
208
 
184
- public static async init(pl: PlClient, root: ResourceId, ops: SynchronizedTreeOps) {
185
- const tree = new SynchronizedTreeState(pl, root, ops);
209
+ public static async init(
210
+ pl: PlClient,
211
+ root: ResourceId,
212
+ ops: SynchronizedTreeOps,
213
+ logger?: MiLogger
214
+ ) {
215
+ const tree = new SynchronizedTreeState(pl, root, ops, logger);
186
216
  await tree.refresh();
187
217
  return tree;
188
218
  }