pipeModelBatches.js 3.1 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970
  1. /** Batch lifecycle boundary. A successful load promise does not imply GPU readiness. */
  2. export function createPipeModelBatches({ load, remove, onChange = () => {}, now = Date.now, retryMs = 30000 }) {
  3. const records = new Map()
  4. const state = () => {
  5. const covered = new Set(), incomplete = new Set()
  6. let pending = 0, failed = 0, batches = 0, instances = 0
  7. const byKind = {}
  8. for (const record of records.values()) {
  9. if (record.status === 'pending') pending += 1
  10. if (record.status === 'failed') failed += 1
  11. if (record.status !== 'ready') {
  12. for (const instance of record.group.instances) incomplete.add(instance.id)
  13. continue
  14. }
  15. batches += 1
  16. instances += record.group.instances.length
  17. byKind[record.group.kind] = (byKind[record.group.kind] || 0) + record.group.instances.length
  18. for (const instance of record.group.instances) covered.add(instance.id)
  19. }
  20. return { covered: [...covered].filter((id) => !incomplete.has(id)), pending, failed, batches, instances, byKind }
  21. }
  22. const changed = () => onChange(state())
  23. function dispose(record) {
  24. record.active = false
  25. if (record.model) { remove(record.model); record.model = null }
  26. }
  27. async function sync(groups) {
  28. const wanted = new Set(groups.map((group) => group.key))
  29. for (const [key, record] of records) {
  30. if (!wanted.has(key)) { dispose(record); records.delete(key) }
  31. }
  32. const jobs = []
  33. for (const group of groups) {
  34. const fingerprint = JSON.stringify(group)
  35. const previous = records.get(group.key)
  36. if (previous && previous.fingerprint === fingerprint && (previous.status !== 'failed' || now() - previous.failedAt < retryMs)) continue
  37. if (previous) dispose(previous)
  38. const record = { group, fingerprint, active: true, status: 'pending', model: null }
  39. records.set(group.key, record)
  40. const error = () => {
  41. if (!record.active) return
  42. record.status = 'failed'
  43. record.failedAt = now()
  44. if (record.model) { remove(record.model); record.model = null }
  45. changed()
  46. }
  47. jobs.push((async () => {
  48. try {
  49. const model = await load(group, {
  50. alive: () => record.active,
  51. ready: () => { if (record.active && record.status === 'pending') { record.status = 'ready'; changed() } },
  52. error,
  53. })
  54. if (!record.active || record.status === 'failed') remove(model)
  55. else record.model = model
  56. } catch { error() }
  57. })())
  58. }
  59. changed()
  60. await Promise.all(jobs)
  61. return state()
  62. }
  63. function retain(predicate) {
  64. for (const [key, record] of records) {
  65. if (!predicate(record.group)) { dispose(record); records.delete(key) }
  66. }
  67. changed()
  68. }
  69. return { sync, state, retain, clear: () => sync([]) }
  70. }