@qvac/registry-client 0.2.1 → 0.4.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 +44 -3
- package/bin/cli.js +41 -1
- package/index.d.ts +27 -1
- package/lib/client.js +36 -0
- package/lib/profiler.js +297 -0
- package/package.json +13 -3
package/README.md
CHANGED
|
@@ -139,11 +139,9 @@ The package includes a CLI for querying and downloading models from the registry
|
|
|
139
139
|
|
|
140
140
|
### Install
|
|
141
141
|
|
|
142
|
-
The package is hosted on npm.
|
|
142
|
+
The package is hosted on npm. Install globally:
|
|
143
143
|
|
|
144
144
|
```bash
|
|
145
|
-
echo "@qvac:registry=https://registry.npmjs.org" >> ~/.npmrc
|
|
146
|
-
echo "//registry.npmjs.org/:_authToken=YOUR_NPM_TOKEN" >> ~/.npmrc
|
|
147
145
|
npm install -g @qvac/registry-client
|
|
148
146
|
```
|
|
149
147
|
|
|
@@ -226,6 +224,47 @@ Downloading ... -> /absolute/path/ggml-tiny-q8_0.bin
|
|
|
226
224
|
Download complete: 41.52 MB
|
|
227
225
|
```
|
|
228
226
|
|
|
227
|
+
### Profile download performance
|
|
228
|
+
|
|
229
|
+
Diagnose slow downloads by collecting UDX network stats, connection info, and hypercore metrics — similar to [hyperdrive-profiler](https://github.com/holepunchto/hyperdrive-profiler) but for registry Hyperblobs:
|
|
230
|
+
|
|
231
|
+
```bash
|
|
232
|
+
$ qvac-registry profile \
|
|
233
|
+
"ggerganov/whisper.cpp/resolve/5359861c739e955e79d9a303bcbc70fb988958b1/ggml-tiny.bin" hf
|
|
234
|
+
|
|
235
|
+
--- 5.0s elapsed ---
|
|
236
|
+
Network (UDX)
|
|
237
|
+
Bytes received: 42.1MB (8.4MB/s)
|
|
238
|
+
Bytes transmitted: 128kB (25.6kB/s)
|
|
239
|
+
Packets rx/tx: 29034 / 1842
|
|
240
|
+
Packets dropped: 0
|
|
241
|
+
Connection
|
|
242
|
+
Firewalled: false
|
|
243
|
+
Blob peers: 2
|
|
244
|
+
Issues: rto=0 fast-recoveries=0 retransmits=0
|
|
245
|
+
Hypercore
|
|
246
|
+
Blob core: 665 / 665 (contiguous / length)
|
|
247
|
+
Hotswaps: 0
|
|
248
|
+
...
|
|
249
|
+
==================================================
|
|
250
|
+
FINAL SUMMARY
|
|
251
|
+
==================================================
|
|
252
|
+
Download
|
|
253
|
+
Model: ggerganov/whisper.cpp/resolve/.../ggml-tiny.bin
|
|
254
|
+
Size: 73.5MB (1120 blocks)
|
|
255
|
+
Metadata: 2.15s
|
|
256
|
+
Transfer: 8.72s
|
|
257
|
+
Avg speed: 8.4MB/s
|
|
258
|
+
Total: 10.87s
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
Flags:
|
|
262
|
+
|
|
263
|
+
```
|
|
264
|
+
--interval|-i [seconds] Stats print interval (default: 5)
|
|
265
|
+
--timeout|-t [ms] Stream read timeout (default: 120000)
|
|
266
|
+
```
|
|
267
|
+
|
|
229
268
|
### JSON output
|
|
230
269
|
|
|
231
270
|
All commands support `--json` for machine-readable output:
|
|
@@ -250,6 +289,7 @@ See the `examples/` folder for complete working examples:
|
|
|
250
289
|
- `download-model.js`: Download a single model to disk via metadata lookup
|
|
251
290
|
- `download-blob.js`: Download a blob directly using known blob coordinates
|
|
252
291
|
- `download-all-models.js`: Download all models in the registry
|
|
292
|
+
- `profile-download.js`: Profile download performance with network/connection/hypercore stats
|
|
253
293
|
|
|
254
294
|
Run examples:
|
|
255
295
|
|
|
@@ -259,6 +299,7 @@ node examples/example.js
|
|
|
259
299
|
node examples/download-model.js
|
|
260
300
|
node examples/download-blob.js
|
|
261
301
|
node examples/download-all-models.js
|
|
302
|
+
node examples/profile-download.js "model/path"
|
|
262
303
|
```
|
|
263
304
|
|
|
264
305
|
## Configuration
|
package/bin/cli.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
const { command, flag, arg, summary, header, footer, description } = require('paparam')
|
|
5
5
|
const { QVACRegistryClient } = require('../index')
|
|
6
|
+
const { profileDownload } = require('../lib/profiler')
|
|
6
7
|
const IdEnc = require('hypercore-id-encoding')
|
|
7
8
|
const path = require('#path')
|
|
8
9
|
|
|
@@ -191,6 +192,44 @@ const downloadCmd = command('download',
|
|
|
191
192
|
}
|
|
192
193
|
)
|
|
193
194
|
|
|
195
|
+
const profileCmd = command('profile',
|
|
196
|
+
summary('Profile download performance for a model'),
|
|
197
|
+
description('Downloads a model to a temp directory while collecting UDX network stats, connection info, and hypercore metrics. Similar to hyperdrive-profiler but for registry blobs.'),
|
|
198
|
+
arg('<path>', 'Model path'),
|
|
199
|
+
arg('[source]', 'Model source filter (e.g. hf, s3)'),
|
|
200
|
+
flag('--interval|-i [seconds]', 'Stats print interval in seconds (default: 5)'),
|
|
201
|
+
flag('--timeout|-t [ms]', 'Stream read timeout in ms (default: 120000)'),
|
|
202
|
+
async function (cmd) {
|
|
203
|
+
const rootFlags = getRootFlags(cmd)
|
|
204
|
+
const registryCoreKey = rootFlags.key || process.env.QVAC_REGISTRY_CORE_KEY
|
|
205
|
+
|
|
206
|
+
if (!registryCoreKey) {
|
|
207
|
+
console.error('Registry core key is required. Set QVAC_REGISTRY_CORE_KEY or use --key.')
|
|
208
|
+
process.exit(1)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const interval = cmd.flags.interval ? parseInt(cmd.flags.interval, 10) : 5
|
|
212
|
+
const timeout = cmd.flags.timeout ? parseInt(cmd.flags.timeout, 10) : 120000
|
|
213
|
+
|
|
214
|
+
if (Number.isNaN(interval) || interval <= 0) {
|
|
215
|
+
console.error('--interval must be a positive number')
|
|
216
|
+
process.exit(1)
|
|
217
|
+
}
|
|
218
|
+
if (Number.isNaN(timeout) || timeout <= 0) {
|
|
219
|
+
console.error('--timeout must be a positive number')
|
|
220
|
+
process.exit(1)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
await profileDownload({
|
|
224
|
+
registryCoreKey,
|
|
225
|
+
modelPath: cmd.args.path,
|
|
226
|
+
source: cmd.args.source || undefined,
|
|
227
|
+
intervalSec: interval,
|
|
228
|
+
timeout
|
|
229
|
+
})
|
|
230
|
+
}
|
|
231
|
+
)
|
|
232
|
+
|
|
194
233
|
// --- Root command ---
|
|
195
234
|
|
|
196
235
|
const cmd = command('qvac-registry',
|
|
@@ -202,7 +241,8 @@ const cmd = command('qvac-registry',
|
|
|
202
241
|
flag('--verbose|-v', 'Enable verbose/debug logging'),
|
|
203
242
|
listCmd,
|
|
204
243
|
getCmd,
|
|
205
|
-
downloadCmd
|
|
244
|
+
downloadCmd,
|
|
245
|
+
profileCmd
|
|
206
246
|
)
|
|
207
247
|
|
|
208
248
|
cmd.parse()
|
package/index.d.ts
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
import ReadyResource = require('ready-resource')
|
|
2
|
+
|
|
3
|
+
export interface LifecycleLogOptions {
|
|
4
|
+
log?: (msg: string) => Promise<void> | void
|
|
5
|
+
}
|
|
6
|
+
|
|
1
7
|
export interface QVACBlobBinding {
|
|
2
8
|
coreKey: Buffer | string
|
|
3
9
|
blockOffset: number
|
|
@@ -41,6 +47,8 @@ export interface QVACDownloadOptions {
|
|
|
41
47
|
timeout?: number
|
|
42
48
|
peerTimeout?: number
|
|
43
49
|
outputFile?: string
|
|
50
|
+
onProgress?: (progress: { downloaded: number, total: number, cachedBlocks: number, totalBlocks: number }) => void
|
|
51
|
+
signal?: AbortSignal
|
|
44
52
|
}
|
|
45
53
|
|
|
46
54
|
export interface QVACBlobDownloadOptions {
|
|
@@ -78,11 +86,29 @@ export interface FindByParams {
|
|
|
78
86
|
includeDeprecated?: boolean
|
|
79
87
|
}
|
|
80
88
|
|
|
81
|
-
export
|
|
89
|
+
export interface LifecycleSwarmHandle {
|
|
90
|
+
readonly suspended: boolean
|
|
91
|
+
suspend (opts?: LifecycleLogOptions): Promise<void>
|
|
92
|
+
resume (opts?: LifecycleLogOptions): Promise<void>
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface LifecycleStoreHandle {
|
|
96
|
+
suspend (opts?: LifecycleLogOptions): Promise<void>
|
|
97
|
+
resume (): Promise<void>
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export class QVACRegistryClient extends ReadyResource {
|
|
82
101
|
constructor (opts?: QVACRegistryClientOptions)
|
|
83
102
|
|
|
103
|
+
/** Valid only while the client remains open. Cached handles become stale after close(). */
|
|
104
|
+
readonly corestore: LifecycleStoreHandle | null
|
|
105
|
+
/** Valid only while the client remains open. Cached handles become stale after close(). */
|
|
106
|
+
readonly hyperswarm: LifecycleSwarmHandle | null
|
|
107
|
+
|
|
84
108
|
ready (): Promise<void>
|
|
85
109
|
close (): Promise<void>
|
|
110
|
+
suspend (opts?: LifecycleLogOptions): Promise<void>
|
|
111
|
+
resume (opts?: LifecycleLogOptions): Promise<void>
|
|
86
112
|
|
|
87
113
|
getModel (path: string, source: string): Promise<QVACModelEntry | null>
|
|
88
114
|
downloadModel (path: string, source: string, options?: QVACDownloadOptions): Promise<QVACDownloadResult>
|
package/lib/client.js
CHANGED
|
@@ -526,6 +526,42 @@ class QVACRegistryClient extends ReadyResource {
|
|
|
526
526
|
}
|
|
527
527
|
}
|
|
528
528
|
|
|
529
|
+
async suspend (opts = {}) {
|
|
530
|
+
this.logger.debug('suspend called')
|
|
531
|
+
|
|
532
|
+
if (!this.opened || this.closing) {
|
|
533
|
+
this.logger.debug('Skipping suspend while client is not open or is closing')
|
|
534
|
+
return
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
if (this.hyperswarm && !this.hyperswarm.suspended) {
|
|
538
|
+
await this.hyperswarm.suspend(opts)
|
|
539
|
+
}
|
|
540
|
+
if (this.corestore) {
|
|
541
|
+
await this.corestore.suspend(opts)
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
this.logger.debug('QVACRegistryClient suspended')
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
async resume (opts = {}) {
|
|
548
|
+
this.logger.debug('resume called')
|
|
549
|
+
|
|
550
|
+
if (!this.opened || this.closing) {
|
|
551
|
+
this.logger.debug('Skipping resume while client is not open or is closing')
|
|
552
|
+
return
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
if (this.corestore) {
|
|
556
|
+
await this.corestore.resume()
|
|
557
|
+
}
|
|
558
|
+
if (this.hyperswarm && this.hyperswarm.suspended) {
|
|
559
|
+
await this.hyperswarm.resume(opts)
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
this.logger.debug('QVACRegistryClient resumed')
|
|
563
|
+
}
|
|
564
|
+
|
|
529
565
|
async _close () {
|
|
530
566
|
this.logger.debug('_close called')
|
|
531
567
|
|
package/lib/profiler.js
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const Corestore = require('corestore')
|
|
4
|
+
const Hyperswarm = require('hyperswarm')
|
|
5
|
+
const Hyperblobs = require('hyperblobs')
|
|
6
|
+
const HypercoreStats = require('hypercore-stats')
|
|
7
|
+
const HyperswarmStats = require('hyperswarm-stats')
|
|
8
|
+
const IdEnc = require('hypercore-id-encoding')
|
|
9
|
+
const byteSize = require('tiny-byte-size')
|
|
10
|
+
const { RegistryDatabase } = require('@qvac/registry-schema')
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Normalize a blob core key into a Buffer regardless of input format.
|
|
14
|
+
* Handles raw Buffer, { data: [...] } objects from HyperDB, and z32/hex strings.
|
|
15
|
+
*/
|
|
16
|
+
function decodeCoreKey (key) {
|
|
17
|
+
if (Buffer.isBuffer(key)) return key
|
|
18
|
+
if (typeof key === 'object' && key !== null && key.data) {
|
|
19
|
+
return Buffer.from(key.data)
|
|
20
|
+
}
|
|
21
|
+
return IdEnc.decode(key)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Collect a snapshot of network, connection, and hypercore stats.
|
|
26
|
+
* All fields are plain values suitable for logging or formatting.
|
|
27
|
+
*/
|
|
28
|
+
function collectStats (swarmStats, hypercoreStats, blobCore, elapsedSec) {
|
|
29
|
+
const bytesRx = swarmStats.dhtStats.udxBytesReceived
|
|
30
|
+
const bytesTx = swarmStats.dhtStats.udxBytesTransmitted
|
|
31
|
+
|
|
32
|
+
const peers = []
|
|
33
|
+
for (const peer of blobCore.peers) {
|
|
34
|
+
peers.push({
|
|
35
|
+
key: IdEnc.normalize(peer.remotePublicKey),
|
|
36
|
+
remoteLength: peer.remoteLength,
|
|
37
|
+
remoteContiguous: peer.remoteContiguousLength
|
|
38
|
+
})
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
elapsedSec,
|
|
43
|
+
network: {
|
|
44
|
+
bytesRx,
|
|
45
|
+
bytesTx,
|
|
46
|
+
rxPerSec: elapsedSec > 0 ? bytesRx / elapsedSec : 0,
|
|
47
|
+
txPerSec: elapsedSec > 0 ? bytesTx / elapsedSec : 0,
|
|
48
|
+
packetsRx: swarmStats.dhtStats.udxPacketsReceived,
|
|
49
|
+
packetsTx: swarmStats.dhtStats.udxPacketsTransmitted,
|
|
50
|
+
packetsDropped: swarmStats.dhtStats.udxPacketsDropped
|
|
51
|
+
},
|
|
52
|
+
connection: {
|
|
53
|
+
firewalled: swarmStats.dhtStats.isFirewalled,
|
|
54
|
+
blobPeers: blobCore.peers.length,
|
|
55
|
+
attempted: swarmStats.connects.client.attempted,
|
|
56
|
+
opened: swarmStats.connects.client.opened,
|
|
57
|
+
closed: swarmStats.connects.client.closed,
|
|
58
|
+
rtos: swarmStats.getRTOCountAcrossAllStreams(),
|
|
59
|
+
fastRecoveries: swarmStats.getFastRecoveriesAcrossAllStreams(),
|
|
60
|
+
retransmits: swarmStats.getRetransmitsAcrossAllStreams()
|
|
61
|
+
},
|
|
62
|
+
hypercore: {
|
|
63
|
+
contiguousLength: blobCore.contiguousLength,
|
|
64
|
+
length: blobCore.length,
|
|
65
|
+
hotswaps: hypercoreStats.totalHotswaps
|
|
66
|
+
},
|
|
67
|
+
peers
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Format a stats snapshot into a human-readable string.
|
|
73
|
+
*/
|
|
74
|
+
function formatStats (stats) {
|
|
75
|
+
const n = stats.network
|
|
76
|
+
const c = stats.connection
|
|
77
|
+
const h = stats.hypercore
|
|
78
|
+
|
|
79
|
+
let lines = '--- ' + stats.elapsedSec.toFixed(1) + 's elapsed ---\n'
|
|
80
|
+
lines += 'Network (UDX)\n'
|
|
81
|
+
lines += ' Bytes received: ' + byteSize(n.bytesRx) + ' (' + byteSize(n.rxPerSec) + '/s)\n'
|
|
82
|
+
lines += ' Bytes transmitted: ' + byteSize(n.bytesTx) + ' (' + byteSize(n.txPerSec) + '/s)\n'
|
|
83
|
+
lines += ' Packets rx/tx: ' + n.packetsRx + ' / ' + n.packetsTx + '\n'
|
|
84
|
+
lines += ' Packets dropped: ' + n.packetsDropped + '\n'
|
|
85
|
+
lines += 'Connection\n'
|
|
86
|
+
lines += ' Firewalled: ' + c.firewalled + '\n'
|
|
87
|
+
lines += ' Blob peers: ' + c.blobPeers + '\n'
|
|
88
|
+
lines += ' Attempted: ' + c.attempted + '\n'
|
|
89
|
+
lines += ' Opened: ' + c.opened + '\n'
|
|
90
|
+
lines += ' Closed: ' + c.closed + '\n'
|
|
91
|
+
lines += ' Issues: rto=' + c.rtos + ' fast-recoveries=' + c.fastRecoveries + ' retransmits=' + c.retransmits + '\n'
|
|
92
|
+
lines += 'Hypercore\n'
|
|
93
|
+
lines += ' Blob core: ' + h.contiguousLength + ' / ' + h.length + ' (contiguous / length)\n'
|
|
94
|
+
lines += ' Hotswaps: ' + h.hotswaps + '\n'
|
|
95
|
+
|
|
96
|
+
if (stats.peers.length > 0) {
|
|
97
|
+
lines += ' Peers:\n'
|
|
98
|
+
for (const p of stats.peers) {
|
|
99
|
+
lines += ' ' + p.key + ' remote=' + p.remoteContiguous + '/' + p.remoteLength + '\n'
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return lines
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Format a final download summary into a human-readable string.
|
|
108
|
+
*/
|
|
109
|
+
function formatSummary (opts) {
|
|
110
|
+
let lines = '='.repeat(50) + '\n'
|
|
111
|
+
lines += 'FINAL SUMMARY\n'
|
|
112
|
+
lines += '='.repeat(50) + '\n'
|
|
113
|
+
lines += 'Download\n'
|
|
114
|
+
lines += ' Model: ' + opts.modelPath + '\n'
|
|
115
|
+
lines += ' Size: ' + byteSize(opts.totalBytes) + ' (' + opts.totalBlocks + ' blocks)\n'
|
|
116
|
+
lines += ' Metadata: ' + opts.metadataSec.toFixed(2) + 's\n'
|
|
117
|
+
lines += ' Transfer: ' + opts.transferSec.toFixed(2) + 's\n'
|
|
118
|
+
lines += ' Avg speed: ' + byteSize(opts.avgSpeed) + '/s\n'
|
|
119
|
+
lines += ' Total: ' + opts.totalSec.toFixed(2) + 's\n'
|
|
120
|
+
return lines
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Profile a blob download from the registry, printing periodic stats.
|
|
125
|
+
*
|
|
126
|
+
* @param {object} opts
|
|
127
|
+
* @param {string} opts.registryCoreKey - Registry view core key (z32 or hex)
|
|
128
|
+
* @param {string} opts.modelPath - Model path in the registry
|
|
129
|
+
* @param {string} [opts.source] - Source filter (e.g. "hf", "s3")
|
|
130
|
+
* @param {number} [opts.intervalSec=5] - Stats print interval in seconds
|
|
131
|
+
* @param {number} [opts.timeout=120000] - Stream read timeout in ms
|
|
132
|
+
* @param {function} [opts.onStats] - Called with (formattedString, rawStats) on each interval
|
|
133
|
+
* @param {function} [opts.onLog] - Called with (message) for log output; defaults to console.log
|
|
134
|
+
* @returns {Promise<object>} Summary with timing and stats
|
|
135
|
+
*/
|
|
136
|
+
async function profileDownload (opts) {
|
|
137
|
+
// Lazy-loaded: these Node builtins don't exist in Bare runtime,
|
|
138
|
+
// so they must not be required at the top level or unit tests break.
|
|
139
|
+
const os = require('#os')
|
|
140
|
+
const fs = require('#fs')
|
|
141
|
+
const path = require('#path')
|
|
142
|
+
const { performance } = require('perf_hooks')
|
|
143
|
+
const { pipeline } = require('stream/promises')
|
|
144
|
+
|
|
145
|
+
const {
|
|
146
|
+
registryCoreKey,
|
|
147
|
+
modelPath,
|
|
148
|
+
source,
|
|
149
|
+
intervalSec = 5,
|
|
150
|
+
timeout = 120000,
|
|
151
|
+
onStats,
|
|
152
|
+
onLog = console.log
|
|
153
|
+
} = opts
|
|
154
|
+
|
|
155
|
+
if (!registryCoreKey) throw new Error('registryCoreKey is required')
|
|
156
|
+
if (!modelPath) throw new Error('modelPath is required')
|
|
157
|
+
|
|
158
|
+
const tStart = performance.now()
|
|
159
|
+
const tmpdir = path.join(os.tmpdir(), 'qvac-profile-' + Date.now())
|
|
160
|
+
fs.mkdirSync(tmpdir, { recursive: true })
|
|
161
|
+
|
|
162
|
+
const store = new Corestore(tmpdir)
|
|
163
|
+
await store.ready()
|
|
164
|
+
|
|
165
|
+
const swarm = new Hyperswarm()
|
|
166
|
+
const hcStats = await HypercoreStats.fromCorestore(store, { cacheExpiryMs: 1000 })
|
|
167
|
+
const swStats = new HyperswarmStats(swarm)
|
|
168
|
+
|
|
169
|
+
swarm.on('connection', (conn, peerInfo) => {
|
|
170
|
+
const key = peerInfo?.publicKey ? IdEnc.normalize(peerInfo.publicKey) : 'unknown'
|
|
171
|
+
onLog(' [conn] peer ' + key + ' connected')
|
|
172
|
+
store.replicate(conn)
|
|
173
|
+
conn.on('error', (e) => onLog(' [conn] error: ' + e.message))
|
|
174
|
+
conn.on('close', () => onLog(' [conn] peer ' + key + ' disconnected'))
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
const cleanupResources = async () => {
|
|
178
|
+
await swarm.destroy()
|
|
179
|
+
await store.close()
|
|
180
|
+
try { fs.rmSync(tmpdir, { recursive: true, force: true }) } catch {}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
try {
|
|
184
|
+
const viewKey = IdEnc.decode(registryCoreKey)
|
|
185
|
+
const viewCore = store.get({ key: viewKey })
|
|
186
|
+
await viewCore.ready()
|
|
187
|
+
|
|
188
|
+
onLog('View core key: ' + IdEnc.normalize(viewCore.key))
|
|
189
|
+
|
|
190
|
+
const foundPeers = viewCore.findingPeers()
|
|
191
|
+
swarm.join(viewCore.discoveryKey, { client: true, server: false })
|
|
192
|
+
swarm.flush().then(() => foundPeers())
|
|
193
|
+
|
|
194
|
+
await viewCore.update()
|
|
195
|
+
const metadataSec = (performance.now() - tStart) / 1000
|
|
196
|
+
|
|
197
|
+
const db = new RegistryDatabase(viewCore, { extension: false })
|
|
198
|
+
await db.ready()
|
|
199
|
+
|
|
200
|
+
onLog('Metadata synced in ' + metadataSec.toFixed(2) + 's (' + viewCore.length + ' blocks)')
|
|
201
|
+
|
|
202
|
+
const model = await db.getModel(modelPath, source || undefined)
|
|
203
|
+
if (!model) throw new Error('Model not found: ' + modelPath)
|
|
204
|
+
|
|
205
|
+
const blob = model.blobBinding
|
|
206
|
+
if (!blob || !blob.coreKey) throw new Error('Model has no blob binding')
|
|
207
|
+
|
|
208
|
+
onLog('\nProfiling download: ' + model.path)
|
|
209
|
+
onLog(' engine: ' + model.engine)
|
|
210
|
+
onLog(' source: ' + model.source)
|
|
211
|
+
onLog(' size: ' + byteSize(blob.byteLength) + ' (' + blob.blockLength + ' blocks)')
|
|
212
|
+
onLog('')
|
|
213
|
+
|
|
214
|
+
const coreKeyBuf = decodeCoreKey(blob.coreKey)
|
|
215
|
+
const blobCore = store.get({ key: coreKeyBuf })
|
|
216
|
+
await blobCore.ready()
|
|
217
|
+
const blobs = new Hyperblobs(blobCore)
|
|
218
|
+
await blobs.ready()
|
|
219
|
+
|
|
220
|
+
onLog('Blob core key: ' + IdEnc.normalize(blobCore.key))
|
|
221
|
+
onLog('Blob core discovery: ' + IdEnc.normalize(blobCore.discoveryKey))
|
|
222
|
+
|
|
223
|
+
const foundBlobPeers = blobCore.findingPeers()
|
|
224
|
+
swarm.join(blobCore.discoveryKey, { client: true, server: false })
|
|
225
|
+
swarm.flush().then(() => foundBlobPeers())
|
|
226
|
+
|
|
227
|
+
await blobCore.update()
|
|
228
|
+
onLog('Blob core synced: length=' + blobCore.length)
|
|
229
|
+
onLog('')
|
|
230
|
+
|
|
231
|
+
const outputFile = path.join(tmpdir, 'download.bin')
|
|
232
|
+
const tDownloadStart = performance.now()
|
|
233
|
+
|
|
234
|
+
const emitStats = () => {
|
|
235
|
+
const elapsed = (performance.now() - tDownloadStart) / 1000
|
|
236
|
+
const raw = collectStats(swStats, hcStats, blobCore, elapsed)
|
|
237
|
+
const formatted = formatStats(raw)
|
|
238
|
+
if (onStats) {
|
|
239
|
+
onStats(formatted, raw)
|
|
240
|
+
} else {
|
|
241
|
+
onLog(formatted)
|
|
242
|
+
}
|
|
243
|
+
return raw
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const statsInterval = setInterval(emitStats, intervalSec * 1000)
|
|
247
|
+
|
|
248
|
+
const rangeDownload = blobCore.download({
|
|
249
|
+
start: blob.blockOffset,
|
|
250
|
+
length: blob.blockLength
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
const readStream = blobs.createReadStream(blob, { wait: true, timeout })
|
|
254
|
+
const writeStream = fs.createWriteStream(outputFile)
|
|
255
|
+
|
|
256
|
+
try {
|
|
257
|
+
await pipeline(readStream, writeStream)
|
|
258
|
+
} finally {
|
|
259
|
+
rangeDownload.destroy()
|
|
260
|
+
clearInterval(statsInterval)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const totalSec = (performance.now() - tStart) / 1000
|
|
264
|
+
const transferSec = (performance.now() - tDownloadStart) / 1000
|
|
265
|
+
const avgSpeed = transferSec > 0 ? blob.byteLength / transferSec : 0
|
|
266
|
+
|
|
267
|
+
const finalStats = emitStats()
|
|
268
|
+
|
|
269
|
+
const summary = {
|
|
270
|
+
modelPath: model.path,
|
|
271
|
+
totalBytes: blob.byteLength,
|
|
272
|
+
totalBlocks: blob.blockLength,
|
|
273
|
+
metadataSec,
|
|
274
|
+
transferSec,
|
|
275
|
+
avgSpeed,
|
|
276
|
+
totalSec,
|
|
277
|
+
finalStats
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
onLog(formatSummary(summary))
|
|
281
|
+
|
|
282
|
+
await blobs.close()
|
|
283
|
+
await blobCore.close()
|
|
284
|
+
|
|
285
|
+
return summary
|
|
286
|
+
} finally {
|
|
287
|
+
await cleanupResources()
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
module.exports = {
|
|
292
|
+
decodeCoreKey,
|
|
293
|
+
collectStats,
|
|
294
|
+
formatStats,
|
|
295
|
+
formatSummary,
|
|
296
|
+
profileDownload
|
|
297
|
+
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qvac/registry-client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "QVAC Registry client library for read-only queries via Hyperswarm",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/tetherto/qvac.git",
|
|
9
|
+
"directory": "packages/qvac-lib-registry-server/client"
|
|
10
|
+
},
|
|
11
|
+
"bugs": "https://github.com/tetherto/qvac/issues",
|
|
12
|
+
"homepage": "https://github.com/tetherto/qvac/tree/main/packages/qvac-lib-registry-server/client#readme",
|
|
6
13
|
"type": "commonjs",
|
|
7
14
|
"main": "index.js",
|
|
8
15
|
"bin": {
|
|
@@ -46,7 +53,7 @@
|
|
|
46
53
|
},
|
|
47
54
|
"dependencies": {
|
|
48
55
|
"@qvac/error": "^0.1.0",
|
|
49
|
-
"@qvac/registry-schema": "^0.1.
|
|
56
|
+
"@qvac/registry-schema": "^0.1.2",
|
|
50
57
|
"b4a": "^1.6.7",
|
|
51
58
|
"bare-fs": "^4.5.2",
|
|
52
59
|
"bare-os": "^3.6.2",
|
|
@@ -55,10 +62,13 @@
|
|
|
55
62
|
"corestore": "^7.4.5",
|
|
56
63
|
"hyperblobs": "^2.8.0",
|
|
57
64
|
"hypercore-id-encoding": "^1.3.0",
|
|
65
|
+
"hypercore-stats": "^2.4.0",
|
|
58
66
|
"hyperdb": "^4.16.1",
|
|
59
67
|
"hyperswarm": "^4.14.0",
|
|
68
|
+
"hyperswarm-stats": "^1.3.0",
|
|
60
69
|
"paparam": "^1.10.0",
|
|
61
|
-
"ready-resource": "^1.0.1"
|
|
70
|
+
"ready-resource": "^1.0.1",
|
|
71
|
+
"tiny-byte-size": "^1.1.0"
|
|
62
72
|
},
|
|
63
73
|
"devDependencies": {
|
|
64
74
|
"brittle": "^3.4.0",
|