@beam-network/sdk 0.6.1 → 0.7.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 CHANGED
@@ -195,3 +195,24 @@ Multipart control helpers (`createMultipartUpload`, `listMultipartParts`, `compl
195
195
  Route streaming flushes at 1,024 routes, with no timer, and sends the final partial batch at stream completion. Signing overlaps acknowledged publication through bounded buffering; existing encoded-payload limits still apply. A source grant is reused across destinations only within the same signing generation and actual expiry.
196
196
 
197
197
  The optional `onDiagnostics` callback receives bounded preparation measurements and source-grant reuse counts. Callbacks are best effort, may be dropped under load, and do not affect transfer outcomes. Durations may overlap; do not sum them into elapsed transfer time. Transfer status also exposes a typed `performance` summary when supported by Core.
198
+
199
+ Optional `storage_location` (`StorageLocation` in Go) describes the physical
200
+ storage location. It is separate from the region used to sign provider requests.
201
+ Leave it unset when unknown; a signing region such as R2's `auto` is not a location.
202
+
203
+ When supported by Core, diagnostics use `sdk-performance/v2`: bounded histograms,
204
+ preparation milestones, concurrency high-water marks, and separate provider,
205
+ callback, manifest, signing, and transport waits. Histogram bins are noncumulative,
206
+ with upper bounds in milliseconds of 0.1, 0.5, 1, 2, 5, 10, 25, 50, 100, 250,
207
+ 500, 1000, 2500, 5000, 10000, 30000, 120000, then overflow. `unmeasured` explicitly
208
+ identifies unavailable measurements. Process CPU includes other concurrent work
209
+ in the SDK process; it is not transfer-exclusive CPU. Detailed reporting can be
210
+ disabled with `BEAM_SDK_PERFORMANCE_DETAILS=false`. Older peers retain v1 reports.
211
+ No credentials or storage grants are included in these diagnostics.
212
+
213
+ `sdk.producer_wait` measures waiting for a signed route separately from publication
214
+ backpressure. Configured-limit gauges preserve starting limits; effective-limit
215
+ gauges report the highest limit reached. Optional `source_renewals` counts source
216
+ grants recreated in later preparation generations. A bounded 16 KiB history tracks
217
+ 131,072 chunk indices; the counter is omitted after takeover or beyond that bound.
218
+ It does not include separate recovery-control signing operations.
package/dist/client.js CHANGED
@@ -1,4 +1,4 @@
1
- import { SdkPerformanceCollector } from "./performance.js";
1
+ import { SdkPerformanceCollector, SourceSignatureHistory, currentSdkPerformance, measureSdkPhase } from "./performance.js";
2
2
  import { signMultipartRecovery, abortMultipartUpload, createMultipartUpload, expiresAtIso, prepareProviderDestination, prepareProviderSource, releaseProviderClients, signAbortMultipartUpload, signCompleteMultipartUpload, signDestinationReadRange, signDestinationRoute, signFinalObjectHead, signListMultipartUpload, signSourceReadRange, signSourceChunk, boundedGrantExpiry, isHuggingFaceProvider } from "./provider-signing.js";
3
3
  import { describe as describeHuggingFace, hashSourceStream, huggingFaceCommit, huggingFaceCompleteLfsUpload, huggingFaceLfsBatch, huggingFacePreupload, huggingFaceVerifyLfsUpload, readSourceSample } from "./huggingface.js";
4
4
  import { BeamTransferControl, BEAM_DEFAULT_NATS_URL, compactSignedRoutes, isRecoverableRouteStreamError } from "./nats-control.js";
@@ -300,10 +300,9 @@ export class BeamClient {
300
300
  destinationsById.set(preparedDestination.destination_id, destination);
301
301
  });
302
302
  await input.throwIfCancelled?.();
303
- const discoveryStarted = performance.now();
304
- const preparedSources = await Promise.all(retainedRecoveryInput.sources.map((source, index) => prepareProviderSource(source, { index, expiresIn, fetchImpl: this.fetchImpl, signal: input.signal })));
303
+ const discoveryTelemetry = new SdkPerformanceCollector();
304
+ const preparedSources = await discoveryTelemetry.measure("sdk.discovery", () => Promise.all(retainedRecoveryInput.sources.map((source, index) => prepareProviderSource(source, { index, expiresIn, fetchImpl: this.fetchImpl, signal: input.signal }))));
305
305
  await input.throwIfCancelled?.();
306
- const discoveryMs = performance.now() - discoveryStarted;
307
306
  const huggingFace = await this.planHuggingFaceUploads({
308
307
  sources: retainedRecoveryInput.sources,
309
308
  preparedSources,
@@ -353,13 +352,18 @@ export class BeamClient {
353
352
  const assertOwnership = () => input.signal?.throwIfAborted();
354
353
  const routeStreamLock = new AsyncMutex();
355
354
  const releaseInitialStream = await routeStreamLock.acquire();
355
+ const sourceSignatureHistory = new SourceSignatureHistory(!resume);
356
356
  const streamPreparedRoutes = async (routeGenerationId, recoveryReplay) => {
357
- const telemetry = new SdkPerformanceCollector();
358
- if (!recoveryReplay)
359
- telemetry.observe("sdk.discovery", discoveryMs);
357
+ const telemetry = new SdkPerformanceCollector(sourceSignatureHistory);
358
+ telemetry.seedDiscovery(discoveryTelemetry);
359
+ telemetry.gauge("signing_configured_limit", this.routeSigningConcurrency);
360
+ telemetry.gauge("multipart_configured_limit", this.multipartControlConcurrency);
361
+ telemetry.gauge("signing_limit", this.routeSigningConcurrency);
362
+ telemetry.gauge("multipart_limit", this.multipartControlConcurrency);
360
363
  const pendingRoutes = new Set();
361
364
  let signingConcurrency = this.routeSigningConcurrency;
362
365
  let signedInWindow = 0;
366
+ let activeSigning = 0;
363
367
  let signingWindowStartedAt = performance.now();
364
368
  let routeStream = null;
365
369
  let routeStreamBeginAttempted = false;
@@ -420,14 +424,14 @@ export class BeamClient {
420
424
  assertOwnership();
421
425
  recoveryMultipartUploads.set(state.manifest.multipart_group_id, state);
422
426
  validateMultipartGroupManifest([state.manifest], prepared.transfer_id);
423
- await input.onMultipartGroupReady?.(multipartGroupIdentity(prepared.transfer_id, state.manifest));
427
+ await telemetry.measure("sdk.multipart_callback", async () => input.onMultipartGroupReady?.(multipartGroupIdentity(prepared.transfer_id, state.manifest)));
424
428
  await routeStream.addManifestGroups([state.manifest]);
425
429
  multipartGroupWaiters.get(state.manifest.multipart_group_id)?.resolve(state);
426
430
  },
427
431
  onGroupFailed: (groupId, error) => multipartGroupWaiters.get(groupId)?.reject(error)
428
432
  }));
429
433
  const streamNextCompletedRoute = async () => {
430
- const settled = await Promise.race([...pendingRoutes].map((pending) => pending.then((route) => ({ pending, route }))));
434
+ const settled = await telemetry.measure("sdk.producer_wait", () => Promise.race([...pendingRoutes].map((pending) => pending.then((route) => ({ pending, route })))));
431
435
  pendingRoutes.delete(settled.pending);
432
436
  assertOwnership();
433
437
  await routeStream.addRoute(settled.route);
@@ -436,6 +440,8 @@ export class BeamClient {
436
440
  const elapsedMs = performance.now() - signingWindowStartedAt;
437
441
  if (!this.routeSigningConcurrencyOverridden && elapsedMs > 4_000 && signingConcurrency < 256) {
438
442
  signingConcurrency = Math.min(256, signingConcurrency * 2);
443
+ telemetry.increment("concurrency_changes");
444
+ telemetry.gauge("signing_limit", signingConcurrency);
439
445
  }
440
446
  signedInWindow = 0;
441
447
  signingWindowStartedAt = performance.now();
@@ -445,10 +451,9 @@ export class BeamClient {
445
451
  await throwIfCancelled?.(prepared.transfer_id);
446
452
  // This promise belongs to this chunk and signing generation only. Its
447
453
  // immutable credential/range identity and actual expiry cannot be renewed.
448
- const sourceGrant = signSourceChunk({ source: sourcesById.get(chunk.source_id), fallbackUrl: chunk.source_url, chunk, expiresIn, fetchImpl: this.fetchImpl });
454
+ let sourceUses = 0;
455
+ const sourceGrant = telemetry.measure("sdk.source_signing", () => signSourceChunk({ source: sourcesById.get(chunk.source_id), fallbackUrl: chunk.source_url, chunk, expiresIn, fetchImpl: this.fetchImpl })).then(grant => { telemetry.sourceCreated(chunk.chunk_index); return grant; });
449
456
  void sourceGrant.catch(() => { });
450
- telemetry.counters.source_signatures++;
451
- telemetry.counters.source_reuses += Math.max(0, chunk.destinations.length - 1);
452
457
  for (const target of chunk.destinations) {
453
458
  const pendingRoute = (async () => {
454
459
  await throwIfCancelled?.(prepared.transfer_id);
@@ -465,14 +470,16 @@ export class BeamClient {
465
470
  const multipartGroupId = multipartGroupStateKey(prepared.transfer_id, target.destination_id, chunk.source_id, finalObjectKey);
466
471
  const upload = isDirectPutDestination(destination)
467
472
  ? undefined
468
- : await multipartGroupWaiters.get(multipartGroupId)?.promise;
473
+ : await telemetry.measure("sdk.multipart_ready_wait", async () => multipartGroupWaiters.get(multipartGroupId)?.promise);
469
474
  if (!isDirectPutDestination(destination) && !upload)
470
475
  throw new Error(`multipart group manifest is missing for ${chunk.source_id}:${target.destination_id}`);
476
+ telemetry.gauge("signing_active_peak", ++activeSigning);
471
477
  return telemetry.measure("sdk.signing", () => this.signProviderRoute({
472
478
  chunk,
473
479
  target,
474
480
  source,
475
- sourceGrant,
481
+ sourceGrant: telemetry.measure("sdk.source_grant_wait", () => sourceGrant).then(grant => { if (sourceUses++ > 0)
482
+ telemetry.counters.source_reuses++; return grant; }),
476
483
  destination,
477
484
  expiresIn,
478
485
  ...(upload ? { upload } : {}),
@@ -483,11 +490,12 @@ export class BeamClient {
483
490
  partNumber: typeof metadata.part_number === "number"
484
491
  ? metadata.part_number
485
492
  : multipartPartNumber(chunk.source_chunk_index),
486
- }));
493
+ })).finally(() => { activeSigning--; });
487
494
  })();
488
495
  pendingRoutes.add(pendingRoute);
496
+ telemetry.gauge("signing_pending_peak", pendingRoutes.size);
489
497
  if (pendingRoutes.size >= signingConcurrency)
490
- await streamNextCompletedRoute();
498
+ await telemetry.measure("sdk.signing_queue", streamNextCompletedRoute);
491
499
  }
492
500
  }
493
501
  while (pendingRoutes.size)
@@ -1252,6 +1260,7 @@ class RouteStreamSender {
1252
1260
  routeCount = 0;
1253
1261
  sendTail = Promise.resolve();
1254
1262
  sendError = null;
1263
+ sending = false;
1255
1264
  constructor(control, options) {
1256
1265
  this.control = control;
1257
1266
  this.options = options;
@@ -1259,6 +1268,7 @@ class RouteStreamSender {
1259
1268
  this.telemetry = options.telemetry ?? new SdkPerformanceCollector();
1260
1269
  }
1261
1270
  async begin() {
1271
+ this.telemetry.startDelaySampling();
1262
1272
  await this.control.request("transfer.route_stream.begin", compact({
1263
1273
  transfer_id: this.options.transferId,
1264
1274
  route_generation_id: this.options.routeGenerationId,
@@ -1275,6 +1285,7 @@ class RouteStreamSender {
1275
1285
  });
1276
1286
  }
1277
1287
  async addManifestGroups(groups) {
1288
+ const started = performance.now();
1278
1289
  if (!groups.length)
1279
1290
  return;
1280
1291
  await Promise.all(groups.map(async (group) => {
@@ -1290,8 +1301,12 @@ class RouteStreamSender {
1290
1301
  idempotencyKey: `transfer:${this.options.transferId}:route-stream:${this.streamId}:manifest:${identity}`
1291
1302
  });
1292
1303
  }));
1304
+ this.telemetry.observe("sdk.manifest_ack", performance.now() - started);
1305
+ this.telemetry.mark("first_manifest_ms");
1293
1306
  }
1294
1307
  async addRoute(route) {
1308
+ this.telemetry.mark("first_route_ms");
1309
+ const assemblyStarted = performance.now();
1295
1310
  const deliveryIndex = signedRouteDeliveryIndex(route);
1296
1311
  if (deliveryIndex === undefined) {
1297
1312
  throw new Error("route delivery_index is required");
@@ -1305,6 +1320,8 @@ class RouteStreamSender {
1305
1320
  this.seenDeliveryIndices.add(deliveryIndex);
1306
1321
  this.routeCount += 1;
1307
1322
  this.batch.push({ ...route, delivery_index: deliveryIndex });
1323
+ this.telemetry.gauge("buffered_batches_peak", 1 + Number(this.sending));
1324
+ this.telemetry.observe("sdk.route_assembly", performance.now() - assemblyStarted);
1308
1325
  if (this.batch.length >= ROUTE_STREAM_BATCH_ROUTES) {
1309
1326
  await this.enqueueFlush();
1310
1327
  }
@@ -1314,12 +1331,14 @@ class RouteStreamSender {
1314
1331
  this.seenDeliveryIndices.size !== this.options.totalRoutes) {
1315
1332
  throw new Error(`route stream has incomplete or duplicate delivery indices: received ${this.routeCount} of ${this.options.totalRoutes} routes`);
1316
1333
  }
1334
+ this.telemetry.mark("final_flush_ms");
1317
1335
  await this.enqueueFlush();
1318
1336
  await this.sendTail;
1319
1337
  if (this.sendError)
1320
1338
  throw this.sendError;
1321
1339
  this.telemetry.observe("sdk.preparation", performance.now() - this.started);
1322
- const sdkPerformance = this.telemetry.snapshot();
1340
+ this.telemetry.finish();
1341
+ const sdkPerformance = this.telemetry.snapshot(this.control.supportsPerformanceV2?.(this.options.transferId) ?? false);
1323
1342
  this.options.onDiagnostics?.(sdkPerformance);
1324
1343
  return this.control.request("transfer.route_stream.complete", {
1325
1344
  sdk_performance: sdkPerformance,
@@ -1341,6 +1360,7 @@ class RouteStreamSender {
1341
1360
  if (this.sendError)
1342
1361
  throw this.sendError;
1343
1362
  const routes = this.batch.splice(0, this.batch.length);
1363
+ const encodingStarted = performance.now();
1344
1364
  const chunks = this.control.splitRoutesForPayload("transfer.route_stream.batch", {
1345
1365
  transfer_id: this.options.transferId,
1346
1366
  route_generation_id: this.options.routeGenerationId,
@@ -1348,19 +1368,27 @@ class RouteStreamSender {
1348
1368
  batch_id: `${this.streamId}:estimate`,
1349
1369
  batch_index: this.batchIndex
1350
1370
  }, routes);
1371
+ this.telemetry.observe("sdk.batch_encode", performance.now() - encodingStarted);
1372
+ this.sending = true;
1351
1373
  for (const chunk of chunks) {
1374
+ const queuedAt = performance.now();
1352
1375
  const batchIndex = this.batchIndex;
1353
1376
  this.batchIndex += 1;
1354
1377
  this.sendTail = this.sendTail.then(async () => {
1378
+ this.telemetry.observe("sdk.batch_queue", performance.now() - queuedAt);
1355
1379
  if (this.sendError)
1356
1380
  return;
1357
1381
  try {
1382
+ const checksumStarted = performance.now();
1358
1383
  const chunkChecksum = new RouteKeysChecksum();
1359
1384
  await chunkChecksum.addMany(chunk.map(routeKeyForSignedRoute));
1360
1385
  this.checksum.merge(chunkChecksum);
1361
1386
  const routeChecksum = chunkChecksum.value();
1362
1387
  const coordinateChecksum = await routeCoordinateChecksum(chunk);
1363
1388
  const batchId = `${this.streamId}:${batchIndex}:${coordinateChecksum}`;
1389
+ this.telemetry.observe("sdk.batch_checksum", performance.now() - checksumStarted);
1390
+ this.telemetry.gauge("batch_routes_max", chunk.length);
1391
+ this.telemetry.mark("first_batch_ms");
1364
1392
  this.telemetry.counters.route_batches++;
1365
1393
  await this.telemetry.measure("sdk.batch_ack", () => this.control.request("transfer.route_stream.batch", {
1366
1394
  transfer_id: this.options.transferId,
@@ -1381,8 +1409,10 @@ class RouteStreamSender {
1381
1409
  }
1382
1410
  });
1383
1411
  }
1412
+ this.sendTail = this.sendTail.finally(() => { this.sending = false; });
1384
1413
  }
1385
1414
  async abort() {
1415
+ this.telemetry.finish();
1386
1416
  this.sendError ??= new Error("route_stream_aborted");
1387
1417
  await this.sendTail;
1388
1418
  }
@@ -1672,7 +1702,12 @@ function restoreProviderMultipartIdentities(prepared, destinations, identities,
1672
1702
  }
1673
1703
  async function createMultipartGroupManifest(input) {
1674
1704
  const groups = input.prepared.plan_descriptor.sources.flatMap((source) => input.prepared.plan_descriptor.destinations.map((destination) => ({ source, destination })));
1705
+ const queuedAt = performance.now();
1706
+ const telemetry = currentSdkPerformance();
1707
+ let active = 0;
1675
1708
  const results = await mapOrderedWithConcurrency(groups, input.concurrency, async ({ source, destination }) => {
1709
+ telemetry?.observe("sdk.multipart_queue", performance.now() - queuedAt);
1710
+ telemetry?.gauge("multipart_active_peak", ++active);
1676
1711
  let multipartGroupId = null;
1677
1712
  try {
1678
1713
  const destinationConfig = input.destinationsById.get(destination.destination_id);
@@ -1690,12 +1725,12 @@ async function createMultipartGroupManifest(input) {
1690
1725
  const finalObjectMetadata = { "beam-transfer-id": input.prepared.transfer_id };
1691
1726
  input.signal?.throwIfAborted();
1692
1727
  const retainedUpload = input.multipartUploads.get(multipartGroupId);
1693
- const uploadId = retainedUpload?.uploadId ?? await createMultipartUpload({
1728
+ const uploadId = retainedUpload?.uploadId ?? await measureSdkPhase("sdk.multipart_provider", () => createMultipartUpload({
1694
1729
  destination: destinationConfig,
1695
1730
  objectKey: finalObjectKey,
1696
1731
  metadata: finalObjectMetadata,
1697
1732
  signal: input.signal
1698
- });
1733
+ }));
1699
1734
  const createdUpload = !retainedUpload;
1700
1735
  if (createdUpload) {
1701
1736
  input.multipartUploads.set(multipartGroupId, {
@@ -1758,6 +1793,9 @@ async function createMultipartGroupManifest(input) {
1758
1793
  input.onGroupFailed(multipartGroupId, error);
1759
1794
  return { ok: false, error };
1760
1795
  }
1796
+ finally {
1797
+ active--;
1798
+ }
1761
1799
  });
1762
1800
  const failure = results.find((result) => !result.ok);
1763
1801
  if (failure && !failure.ok) {
package/dist/models.d.ts CHANGED
@@ -1,16 +1,22 @@
1
1
  export type SignedUrlFlow = "signed_url";
2
2
  export interface SdkPerformanceSummary {
3
- schema_version: "sdk-performance/v1";
3
+ gauges?: Record<string, number>;
4
+ milestones?: Record<string, number>;
5
+ unmeasured?: string[];
6
+ detail_dropped?: number;
7
+ schema_version: "sdk-performance/v1" | "sdk-performance/v2";
4
8
  measurements: Array<{
5
- name: "sdk.discovery" | "sdk.multipart_create" | "sdk.signing" | "sdk.batch_ack" | "sdk.buffer_wait" | "sdk.preparation";
9
+ name: string;
6
10
  count: number;
7
11
  work_ms: number;
8
12
  max_ms: number;
13
+ histogram?: number[];
9
14
  }>;
10
15
  counters: {
11
16
  source_signatures: number;
12
17
  source_reuses: number;
13
18
  route_batches: number;
19
+ source_renewals?: number;
14
20
  };
15
21
  }
16
22
  export interface BeamClientOptions {
@@ -158,6 +164,21 @@ export interface IntegrityAuditChallenge {
158
164
  chunks: IntegrityAuditChallengeChunk[];
159
165
  }
160
166
  export interface TransferPerformance {
167
+ recovery_bandwidth?: {
168
+ excluded_tasks: number;
169
+ accepted_bytes: number;
170
+ };
171
+ deadline?: {
172
+ version: "assignment-deadline/v1";
173
+ decisions: number;
174
+ selected_min_ms: number;
175
+ selected_max_ms: number;
176
+ estimated_max_ms: number;
177
+ sample_count_max: number;
178
+ data_age_max_ms: number | null;
179
+ reasons: Record<string, number>;
180
+ };
181
+ sdk_detail?: SdkPerformanceSummary;
161
182
  schema_version: "transfer-performance/v1";
162
183
  runtime_epoch: string;
163
184
  coverage: "complete" | "restarted";
@@ -177,6 +198,7 @@ export interface TransferPerformance {
177
198
  source_signatures: number;
178
199
  source_reuses: number;
179
200
  route_batches: number;
201
+ source_renewals?: number;
180
202
  };
181
203
  detail_dropped: number;
182
204
  unmeasured?: string[];
@@ -233,6 +255,8 @@ export interface TransferTerminalSignalWaiter {
233
255
  close(): Promise<void>;
234
256
  }
235
257
  export interface S3ProviderConfig {
258
+ /** Physical storage location; independent of the signing region. */
259
+ storage_location?: string;
236
260
  provider: "s3";
237
261
  id?: string;
238
262
  bucket: string;
@@ -244,6 +268,8 @@ export interface S3ProviderConfig {
244
268
  endpoint_url?: string;
245
269
  }
246
270
  export interface R2ProviderConfig {
271
+ /** Physical storage location; independent of the signing region. */
272
+ storage_location?: string;
247
273
  provider: "r2";
248
274
  id?: string;
249
275
  bucket: string;
@@ -254,6 +280,8 @@ export interface R2ProviderConfig {
254
280
  endpoint_url?: string;
255
281
  }
256
282
  export interface S3CompatibleProviderConfig {
283
+ /** Physical storage location; independent of the signing region. */
284
+ storage_location?: string;
257
285
  provider: string;
258
286
  driver?: "s3-compatible";
259
287
  id?: string;
@@ -268,6 +296,8 @@ export interface S3CompatibleProviderConfig {
268
296
  account_id?: string;
269
297
  }
270
298
  export interface HippiusProviderConfig {
299
+ /** Physical storage location; independent of the signing region. */
300
+ storage_location?: string;
271
301
  provider: "hippius";
272
302
  id?: string;
273
303
  bucket: string;
@@ -277,6 +307,8 @@ export interface HippiusProviderConfig {
277
307
  }
278
308
  export type HuggingFaceRepoType = "model" | "dataset" | "space" | "kernel" | "bucket";
279
309
  export interface HuggingFaceProviderConfig {
310
+ /** Physical storage location; independent of the signing region. */
311
+ storage_location?: string;
280
312
  provider: "huggingface";
281
313
  id?: string;
282
314
  /** Namespace and repo or bucket name separated by a slash, for example `org/dataset`. */
@@ -135,6 +135,7 @@ export declare class BeamTransferControl {
135
135
  private readonly recoveryOperations;
136
136
  private readonly recoveryRequestedEpoch;
137
137
  private readonly runtimeEpochs;
138
+ private readonly runtimeCapabilities;
138
139
  private helloTimer;
139
140
  constructor(options: TransferControlOptions);
140
141
  close(): Promise<void>;
@@ -155,6 +156,7 @@ export declare class BeamTransferControl {
155
156
  splitRoutesForPayload(messageType: TransferClientMessageType, basePayload: Record<string, unknown>, routes: SignedChunkRoute[]): SignedChunkRoute[][];
156
157
  private connection;
157
158
  private ensureHelloMonitor;
159
+ supportsPerformanceV2(transferId: string): boolean;
158
160
  private pollRuntimeHello;
159
161
  private observeRuntimeEpoch;
160
162
  private recoverLease;
@@ -1,3 +1,4 @@
1
+ import { currentSdkPerformance } from "./performance.js";
1
2
  import { encode, decode } from "@msgpack/msgpack";
2
3
  import { connect } from "nats";
3
4
  export const TRANSFER_CLIENT_CONTROL_SCHEMA_VERSION = "transfer-client-control/v7";
@@ -65,6 +66,7 @@ export class BeamTransferControl {
65
66
  recoveryOperations = new Map();
66
67
  recoveryRequestedEpoch = new Map();
67
68
  runtimeEpochs = new Map();
69
+ runtimeCapabilities = new Map();
68
70
  helloTimer = null;
69
71
  constructor(options) {
70
72
  this.apiKey = options.apiKey;
@@ -277,7 +279,12 @@ export class BeamTransferControl {
277
279
  producer: "sdk",
278
280
  payload
279
281
  };
282
+ const encodeStarted = performance.now();
280
283
  const bytes = encode(envelope);
284
+ if (messageType === "transfer.route_stream.batch") {
285
+ currentSdkPerformance()?.observe("sdk.batch_encode", performance.now() - encodeStarted);
286
+ currentSdkPerformance()?.gauge("batch_bytes_max", bytes.byteLength);
287
+ }
281
288
  if (bytes.byteLength > this.maxPayloadBytes) {
282
289
  throw new Error(`NATS lifecycle request is ${bytes.byteLength} bytes, above maxPayloadBytes=${this.maxPayloadBytes}`);
283
290
  }
@@ -488,10 +495,15 @@ export class BeamTransferControl {
488
495
  }, 5_000);
489
496
  this.helloTimer.unref?.();
490
497
  }
498
+ supportsPerformanceV2(transferId) {
499
+ const value = this.runtimeCapabilities.get(transferShardId(transferId, this.shardCount));
500
+ return !!value && value.until > Date.now() && value.capabilities.includes("sdk-performance/v2");
501
+ }
491
502
  async pollRuntimeHello(shardId) {
492
503
  try {
493
504
  try {
494
- await this.request("runtime.hello", { capabilities: ["integrity-signing/v1"] }, { idempotencyKey: `runtime:hello:${shardId}:integrity-signing`, shardId });
505
+ const hello = await this.request("runtime.hello", { capabilities: ["integrity-signing/v1", "sdk-performance/v2"] }, { idempotencyKey: `runtime:hello:${shardId}:integrity-signing`, shardId });
506
+ this.runtimeCapabilities.set(shardId, { until: Date.now() + 15_000, capabilities: hello.capabilities ?? [] });
495
507
  }
496
508
  catch {
497
509
  await this.request("runtime.hello", {}, { idempotencyKey: `runtime:hello:${shardId}`, shardId });
@@ -510,6 +522,7 @@ export class BeamTransferControl {
510
522
  return;
511
523
  if (previous.runtimeEpoch === runtimeEpoch && previous.transportEpoch === transportEpoch)
512
524
  return;
525
+ this.runtimeCapabilities.delete(shardId);
513
526
  if (previous.runtimeEpoch !== runtimeEpoch) {
514
527
  this.authToken = null;
515
528
  this.authTokenExpiresAt = 0;
@@ -1,12 +1,39 @@
1
1
  import type { SdkPerformanceSummary } from './models.js';
2
+ export declare const currentSdkPerformance: () => SdkPerformanceCollector | undefined;
3
+ export declare const measureSdkPhase: <T>(name: string, work: () => Promise<T>) => Promise<T>;
4
+ /** Bounded identities only: no URLs, credentials or grant bodies. */
5
+ export declare class SourceSignatureHistory {
6
+ complete: boolean;
7
+ private readonly bits;
8
+ constructor(complete?: boolean);
9
+ created(index: number): boolean;
10
+ }
2
11
  export declare class SdkPerformanceCollector {
12
+ private readonly sourceHistory?;
13
+ constructor(sourceHistory?: SourceSignatureHistory | undefined);
14
+ private sourceRenewals;
15
+ sourceCreated(index: number): void;
16
+ private readonly started;
17
+ private readonly details;
18
+ private readonly cpu;
19
+ private finished;
20
+ private delayTimer?;
3
21
  private readonly measurements;
22
+ private readonly gauges;
23
+ private readonly milestones;
24
+ private dropped;
4
25
  readonly counters: {
5
26
  source_signatures: number;
6
27
  source_reuses: number;
7
28
  route_batches: number;
8
29
  };
9
- observe(name: SdkPerformanceSummary['measurements'][number]['name'], milliseconds: number): void;
10
- measure<T>(name: SdkPerformanceSummary['measurements'][number]['name'], work: () => Promise<T>): Promise<T>;
11
- snapshot(): SdkPerformanceSummary;
30
+ observe(name: string, milliseconds: number): void;
31
+ gauge(name: string, value: number): void;
32
+ increment(name: string): void;
33
+ seedDiscovery(other: SdkPerformanceCollector): void;
34
+ mark(name: string): void;
35
+ measure<T>(name: string, work: () => Promise<T>): Promise<T>;
36
+ startDelaySampling(): void;
37
+ finish(): void;
38
+ snapshot(v2?: boolean): SdkPerformanceSummary;
12
39
  }
@@ -1,25 +1,124 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ const NAMES = ["sdk.discovery", "sdk.multipart_create", "sdk.signing", "sdk.batch_ack", "sdk.buffer_wait", "sdk.preparation", "sdk.source_signing", "sdk.destination_signing", "sdk.source_grant_wait", "sdk.multipart_ready_wait", "sdk.multipart_provider", "sdk.multipart_queue", "sdk.multipart_callback", "sdk.manifest_ack", "sdk.provider_client_setup", "sdk.metadata_request", "sdk.route_assembly", "sdk.batch_encode", "sdk.batch_checksum", "sdk.batch_queue", "sdk.signing_queue", "sdk.process_cpu", "sdk.event_loop_delay", "sdk.producer_wait"];
3
+ const BOUNDS = [0.1, .5, 1, 2, 5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000, 30000, 120000];
4
+ const GAUGES = new Set(["signing_configured_limit", "multipart_configured_limit", "signing_limit", "multipart_limit", "signing_active_peak", "signing_pending_peak", "multipart_active_peak", "buffered_batches_peak", "batch_routes_max", "batch_bytes_max", "concurrency_changes", "source_waiters", "provider_clients_created", "provider_clients_reused"]);
5
+ const MILESTONES = new Set(["first_manifest_ms", "first_route_ms", "first_batch_ms", "final_flush_ms", "prepared_ms"]);
6
+ const context = new AsyncLocalStorage();
7
+ let activeDelaySamplers = 0;
8
+ export const currentSdkPerformance = () => context.getStore();
9
+ export const measureSdkPhase = (name, work) => context.getStore()?.measure(name, work) ?? work();
10
+ /** Bounded identities only: no URLs, credentials or grant bodies. */
11
+ export class SourceSignatureHistory {
12
+ complete;
13
+ bits = new Uint8Array(16_384);
14
+ constructor(complete = true) {
15
+ this.complete = complete;
16
+ }
17
+ created(index) {
18
+ if (!Number.isSafeInteger(index) || index < 0 || index >= this.bits.length * 8) {
19
+ this.complete = false;
20
+ return false;
21
+ }
22
+ const byte = index >>> 3, mask = 1 << (index & 7);
23
+ const renewed = (this.bits[byte] & mask) !== 0;
24
+ this.bits[byte] |= mask;
25
+ return renewed;
26
+ }
27
+ }
1
28
  export class SdkPerformanceCollector {
29
+ sourceHistory;
30
+ constructor(sourceHistory) {
31
+ this.sourceHistory = sourceHistory;
32
+ }
33
+ sourceRenewals = 0;
34
+ sourceCreated(index) {
35
+ this.counters.source_signatures++;
36
+ if (this.sourceHistory?.created(index))
37
+ this.sourceRenewals++;
38
+ }
39
+ started = performance.now();
40
+ details = process.env.BEAM_SDK_PERFORMANCE_DETAILS !== 'false';
41
+ cpu = process.cpuUsage();
42
+ finished = false;
43
+ delayTimer;
2
44
  measurements = new Map();
45
+ gauges = {};
46
+ milestones = {};
47
+ dropped = 0;
3
48
  counters = { source_signatures: 0, source_reuses: 0, route_batches: 0 };
4
49
  observe(name, milliseconds) {
5
- if (!Number.isFinite(milliseconds) || milliseconds < 0)
50
+ const index = NAMES.indexOf(name);
51
+ if (index < 0 || (!this.details && index >= 6) || !Number.isFinite(milliseconds) || milliseconds < 0 || milliseconds > 86_400_000)
52
+ return;
53
+ const value = this.measurements.get(name) ?? { count: 0, work_ms: 0, max_ms: 0, histogram: Array(18).fill(0) };
54
+ if (value.count >= 1_000_000 || value.work_ms + milliseconds > 86_400_000) {
55
+ this.dropped++;
6
56
  return;
7
- const value = this.measurements.get(name) ?? { count: 0, work_ms: 0, max_ms: 0 };
57
+ }
8
58
  value.count++;
9
59
  value.work_ms += milliseconds;
10
60
  value.max_ms = Math.max(value.max_ms, milliseconds);
61
+ const bin = BOUNDS.findIndex(bound => milliseconds <= bound);
62
+ value.histogram[bin < 0 ? 17 : bin]++;
11
63
  this.measurements.set(name, value);
12
64
  }
65
+ gauge(name, value) {
66
+ if (this.details && GAUGES.has(name) && Number.isFinite(value) && value >= 0)
67
+ this.gauges[name] = Math.min(1_000_000_000, Math.max(this.gauges[name] ?? 0, value));
68
+ }
69
+ increment(name) { this.gauge(name, (this.gauges[name] ?? 0) + 1); }
70
+ seedDiscovery(other) {
71
+ for (const name of ["sdk.discovery", "sdk.metadata_request", "sdk.provider_client_setup"]) {
72
+ const value = other.measurements.get(name);
73
+ if (value)
74
+ this.measurements.set(name, { ...value, histogram: [...value.histogram] });
75
+ }
76
+ for (const name of ["provider_clients_created", "provider_clients_reused"])
77
+ if (other.gauges[name] !== undefined)
78
+ this.gauges[name] = other.gauges[name];
79
+ }
80
+ mark(name) {
81
+ if (this.details && MILESTONES.has(name) && this.milestones[name] === undefined)
82
+ this.milestones[name] = performance.now() - this.started;
83
+ }
13
84
  async measure(name, work) {
14
85
  const start = performance.now();
15
86
  try {
16
- return await work();
87
+ return await context.run(this, work);
17
88
  }
18
89
  finally {
19
90
  this.observe(name, performance.now() - start);
20
91
  }
21
92
  }
22
- snapshot() {
23
- return { schema_version: 'sdk-performance/v1', measurements: [...this.measurements].map(([name, value]) => ({ name: name, ...value })), counters: { ...this.counters } };
93
+ startDelaySampling() {
94
+ if (!this.details || this.delayTimer || this.finished || activeDelaySamplers >= 16)
95
+ return;
96
+ activeDelaySamplers++;
97
+ let last = performance.now();
98
+ this.delayTimer = setInterval(() => {
99
+ const now = performance.now();
100
+ this.observe('sdk.event_loop_delay', Math.max(0, now - last - 100));
101
+ last = now;
102
+ }, 100);
103
+ this.delayTimer.unref();
104
+ }
105
+ finish() {
106
+ if (this.finished)
107
+ return;
108
+ this.finished = true;
109
+ if (this.delayTimer) {
110
+ clearInterval(this.delayTimer);
111
+ this.delayTimer = undefined;
112
+ activeDelaySamplers--;
113
+ }
114
+ const used = process.cpuUsage(this.cpu);
115
+ this.observe('sdk.process_cpu', (used.user + used.system) / 1000);
116
+ this.mark('prepared_ms');
117
+ }
118
+ snapshot(v2 = true) {
119
+ const measurements = [...this.measurements].filter(([name]) => v2 || NAMES.slice(0, 6).includes(name))
120
+ .map(([name, value]) => ({ name, count: value.count, work_ms: value.work_ms, max_ms: value.max_ms, ...(v2 ? { histogram: [...value.histogram] } : {}) }));
121
+ return { schema_version: v2 ? 'sdk-performance/v2' : 'sdk-performance/v1', measurements, counters: { ...this.counters, ...(v2 && this.sourceHistory?.complete ? { source_renewals: this.sourceRenewals } : {}) },
122
+ ...(v2 ? { gauges: { ...this.gauges }, milestones: { ...this.milestones }, unmeasured: NAMES.slice(6).filter(name => !this.measurements.has(name)), detail_dropped: this.dropped } : {}) };
24
123
  }
25
124
  }
@@ -1,3 +1,4 @@
1
+ import { currentSdkPerformance, measureSdkPhase } from "./performance.js";
1
2
  import { AbortMultipartUploadCommand, CompleteMultipartUploadCommand, CreateMultipartUploadCommand, GetObjectCommand, HeadObjectCommand, ListPartsCommand, PutObjectCommand, S3Client, UploadPartCommand } from "@aws-sdk/client-s3";
2
3
  import { UploadPartCopyCommand, DeleteObjectCommand, ListObjectsV2Command } from "@aws-sdk/client-s3";
3
4
  import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
@@ -82,7 +83,7 @@ export async function prepareProviderSource(source, options = {}) {
82
83
  if (isS3CompatibleProvider(source)) {
83
84
  const endpoint = s3CompatibleEndpoint(source);
84
85
  const client = createS3CompatibleClient(source, endpoint);
85
- const head = await client.send(new HeadObjectCommand({ Bucket: source.bucket, Key: source.key }), { abortSignal: options.signal });
86
+ const head = await measureSdkPhase("sdk.metadata_request", () => client.send(new HeadObjectCommand({ Bucket: source.bucket, Key: source.key }), { abortSignal: options.signal }));
86
87
  const getUrl = await getSignedUrl(client, new GetObjectCommand({ Bucket: source.bucket, Key: source.key }), { expiresIn });
87
88
  return {
88
89
  source_id: source.id ?? `src_${index}`,
@@ -113,7 +114,7 @@ export async function prepareProviderSource(source, options = {}) {
113
114
  size,
114
115
  filename: filename(source.key),
115
116
  expires_at: expiresAtIso(expiresIn),
116
- metadata: { bucket: source.bucket, key: source.key, base_url: baseUrl }
117
+ metadata: compactMetadata({ bucket: source.bucket, key: source.key, base_url: baseUrl, storage_location: source.storage_location })
117
118
  };
118
119
  }
119
120
  if (isHuggingFaceProvider(source)) {
@@ -161,7 +162,7 @@ export async function prepareProviderSourceForPlan(source, options = {}) {
161
162
  provider: "hippius",
162
163
  size,
163
164
  filename: filename(source.key),
164
- metadata: { bucket: source.bucket, key: source.key, base_url: baseUrl }
165
+ metadata: compactMetadata({ bucket: source.bucket, key: source.key, base_url: baseUrl, storage_location: source.storage_location })
165
166
  };
166
167
  }
167
168
  if (isHuggingFaceProvider(source)) {
@@ -199,6 +200,7 @@ export function prepareProviderDestination(destination, options = {}) {
199
200
  metadata: {
200
201
  bucket: destination.bucket,
201
202
  key: destination.key,
203
+ storage_location: destination.storage_location,
202
204
  base_url: destination.base_url ?? "https://api.hippius.com"
203
205
  }
204
206
  };
@@ -336,14 +338,14 @@ export async function signDestinationRoute(input) {
336
338
  }
337
339
  const destinationExpiresAt = expiresAtIso(input.expiresIn);
338
340
  const [destUrl, sourceRoute] = await Promise.all([
339
- input.destUrl ?? signDestinationUrl({
341
+ input.destUrl ?? measureSdkPhase("sdk.destination_signing", () => signDestinationUrl({
340
342
  destination: input.destination,
341
343
  objectKey: targetObjectKey,
342
344
  uploadId: input.uploadId,
343
345
  partNumber: input.partNumber,
344
346
  expiresIn: input.expiresIn,
345
347
  fetchImpl: input.fetchImpl,
346
- }),
348
+ })),
347
349
  input.sourceGrant ?? signSourceChunk({
348
350
  source: input.source,
349
351
  fallbackUrl: input.chunk.source_url,
@@ -522,8 +524,11 @@ function s3ClientCacheKey(source, endpoint) {
522
524
  function createS3CompatibleClient(source, endpoint = s3CompatibleEndpoint(source)) {
523
525
  const cacheKey = s3ClientCacheKey(source, endpoint);
524
526
  const cached = s3ClientCache.get(source);
525
- if (cached?.identity === cacheKey)
527
+ if (cached?.identity === cacheKey) {
528
+ currentSdkPerformance()?.increment("provider_clients_reused");
526
529
  return cached.client;
530
+ }
531
+ const setupStarted = performance.now();
527
532
  const client = new S3Client({
528
533
  region: s3CompatibleRegion(source),
529
534
  endpoint,
@@ -536,6 +541,8 @@ function createS3CompatibleClient(source, endpoint = s3CompatibleEndpoint(source
536
541
  }
537
542
  });
538
543
  s3ClientCache.set(source, { identity: cacheKey, client });
544
+ currentSdkPerformance()?.increment("provider_clients_created");
545
+ currentSdkPerformance()?.observe("sdk.provider_client_setup", performance.now() - setupStarted);
539
546
  return client;
540
547
  }
541
548
  export function isS3CompatibleProvider(config) {
@@ -586,6 +593,7 @@ function s3CompatibleMetadata(source, endpoint = s3CompatibleEndpoint(source)) {
586
593
  bucket: source.bucket,
587
594
  key: source.key,
588
595
  region: s3CompatibleRegion(source),
596
+ storage_location: source.storage_location,
589
597
  endpoint_url: endpoint,
590
598
  account_id: "account_id" in source ? source.account_id : undefined
591
599
  });
@@ -599,6 +607,7 @@ export function isHuggingFaceProvider(config) {
599
607
  function huggingFaceMetadata(config, etag, commitHash) {
600
608
  return compactMetadata({
601
609
  driver: "huggingface",
610
+ storage_location: config.storage_location,
602
611
  repo_id: config.repo_id,
603
612
  repo_type: huggingFaceRepoType(config),
604
613
  revision: huggingFaceRevision(config),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@beam-network/sdk",
3
- "version": "0.6.1",
3
+ "version": "0.7.0",
4
4
  "description": "TypeScript SDK for BEAM transfer creation and management.",
5
5
  "type": "module",
6
6
  "license": "MIT",