H-Liu1997 commited on
Commit
a3de023
·
1 Parent(s): 80c3e53

perf: batch frame fetching to overcome HTTP round-trip latency

Browse files
Files changed (2) hide show
  1. app.py +11 -7
  2. static/js/main.js +30 -42
app.py CHANGED
@@ -469,20 +469,24 @@ def get_frame():
469
  if model_manager is None:
470
  return jsonify({"status": "error", "message": "Model not initialized"}), 400
471
 
472
- # Get next frame from buffer
473
- joints = model_manager.get_next_frame()
474
-
475
- if joints is not None:
 
 
 
 
 
 
476
  # Update last consumption time
477
  with consumption_monitor_lock:
478
  last_frame_consumed_time = time.time()
479
 
480
- # Convert numpy array to list for JSON
481
- joints_list = joints.tolist()
482
  return jsonify(
483
  {
484
  "status": "success",
485
- "joints": joints_list,
486
  "buffer_size": model_manager.frame_buffer.size(),
487
  }
488
  )
 
469
  if model_manager is None:
470
  return jsonify({"status": "error", "message": "Model not initialized"}), 400
471
 
472
+ # Get batch of frames from buffer (reduces HTTP round-trip overhead)
473
+ count = min(int(request.args.get("count", 8)), 20)
474
+ frames = []
475
+ for _ in range(count):
476
+ joints = model_manager.get_next_frame()
477
+ if joints is None:
478
+ break
479
+ frames.append(joints.tolist())
480
+
481
+ if frames:
482
  # Update last consumption time
483
  with consumption_monitor_lock:
484
  last_frame_consumed_time = time.time()
485
 
 
 
486
  return jsonify(
487
  {
488
  "status": "success",
489
+ "frames": frames,
490
  "buffer_size": model_manager.frame_buffer.size(),
491
  }
492
  )
static/js/main.js CHANGED
@@ -18,6 +18,10 @@ class MotionApp {
18
  // Request throttling
19
  this.isFetchingFrame = false; // Prevent concurrent requests
20
  this.consecutiveWaiting = 0; // Count consecutive 'waiting' responses
 
 
 
 
21
 
22
  // Session management
23
  this.sessionId = this.generateSessionId();
@@ -473,68 +477,51 @@ class MotionApp {
473
 
474
  fetchFrame() {
475
  if (!this.isRunning) return;
476
-
477
  const now = performance.now();
478
-
479
- // Check if it's time to fetch next frame AND we're not already fetching
480
- if (now >= this.nextFetchTime && !this.isFetchingFrame) {
481
- // Schedule next fetch (maintain fixed rate regardless of delays)
482
  this.nextFetchTime += this.frameInterval;
483
-
484
- // If we've fallen behind, catch up
485
  if (this.nextFetchTime < now) {
486
  this.nextFetchTime = now + this.frameInterval;
487
  }
488
-
489
- // Mark as fetching to prevent concurrent requests
 
 
 
 
 
 
 
 
 
 
 
490
  this.isFetchingFrame = true;
491
-
492
- fetch(`/api/get_frame?session_id=${this.sessionId}`)
493
  .then(response => response.json())
494
  .then(data => {
495
  if (data.status === 'success') {
496
- this.skeleton.updatePose(data.joints);
497
- this.frameCount++;
498
- this.frameCountEl.textContent = this.frameCount;
499
-
500
- // Update motion FPS counter (only when frame consumed)
501
-
502
-
503
- this.motionFpsCounter++;
504
-
505
- // Update current root position
506
- this.currentRootPos.set(
507
- data.joints[0][0],
508
- data.joints[0][1],
509
- data.joints[0][2]
510
- );
511
-
512
- // Auto-follow (if user hasn't interacted for a while)
513
- this.updateAutoFollow();
514
-
515
- // Reset waiting counter on success
516
  this.consecutiveWaiting = 0;
517
  } else if (data.status === 'waiting') {
518
- // No frame available, slow down requests if this happens repeatedly
519
  this.consecutiveWaiting++;
520
-
521
- // If buffer is consistently empty, back off a bit
522
- if (this.consecutiveWaiting > 5) {
523
- // Add a small delay to reduce server load
524
- this.nextFetchTime = now + this.frameInterval * 1.5;
525
- this.consecutiveWaiting = 0;
526
- }
527
  }
528
  })
529
  .catch(error => {
530
- console.error('Error fetching frame:', error);
531
  })
532
  .finally(() => {
533
- // Always mark as done fetching
534
  this.isFetchingFrame = false;
535
  });
536
  }
537
-
538
  // Use requestAnimationFrame for continuous checking
539
  requestAnimationFrame(() => this.fetchFrame());
540
  }
@@ -575,6 +562,7 @@ class MotionApp {
575
  this.motionFpsCounter = 0;
576
  this.isFetchingFrame = false;
577
  this.consecutiveWaiting = 0;
 
578
  this.startResetBtn.textContent = 'Start';
579
  this.startResetBtn.classList.remove('btn-danger');
580
  this.startResetBtn.classList.add('btn-primary');
 
18
  // Request throttling
19
  this.isFetchingFrame = false; // Prevent concurrent requests
20
  this.consecutiveWaiting = 0; // Count consecutive 'waiting' responses
21
+
22
+ // Local frame queue for batch fetching (reduces HTTP round-trip overhead)
23
+ this.localFrameQueue = [];
24
+ this.batchSize = 8; // Fetch up to 8 frames per request
25
 
26
  // Session management
27
  this.sessionId = this.generateSessionId();
 
477
 
478
  fetchFrame() {
479
  if (!this.isRunning) return;
480
+
481
  const now = performance.now();
482
+
483
+ // Play back from local queue at target FPS
484
+ if (now >= this.nextFetchTime && this.localFrameQueue.length > 0) {
 
485
  this.nextFetchTime += this.frameInterval;
 
 
486
  if (this.nextFetchTime < now) {
487
  this.nextFetchTime = now + this.frameInterval;
488
  }
489
+
490
+ const joints = this.localFrameQueue.shift();
491
+ this.skeleton.updatePose(joints);
492
+ this.frameCount++;
493
+ this.frameCountEl.textContent = this.frameCount;
494
+ this.motionFpsCounter++;
495
+
496
+ this.currentRootPos.set(joints[0][0], joints[0][1], joints[0][2]);
497
+ this.updateAutoFollow();
498
+ }
499
+
500
+ // Fetch a batch from server when local queue is running low
501
+ if (this.localFrameQueue.length < this.batchSize && !this.isFetchingFrame) {
502
  this.isFetchingFrame = true;
503
+
504
+ fetch(`/api/get_frame?session_id=${this.sessionId}&count=${this.batchSize}`)
505
  .then(response => response.json())
506
  .then(data => {
507
  if (data.status === 'success') {
508
+ // Push batch of frames into local queue
509
+ for (const frame of data.frames) {
510
+ this.localFrameQueue.push(frame);
511
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
512
  this.consecutiveWaiting = 0;
513
  } else if (data.status === 'waiting') {
 
514
  this.consecutiveWaiting++;
 
 
 
 
 
 
 
515
  }
516
  })
517
  .catch(error => {
518
+ console.error('Error fetching frames:', error);
519
  })
520
  .finally(() => {
 
521
  this.isFetchingFrame = false;
522
  });
523
  }
524
+
525
  // Use requestAnimationFrame for continuous checking
526
  requestAnimationFrame(() => this.fetchFrame());
527
  }
 
562
  this.motionFpsCounter = 0;
563
  this.isFetchingFrame = false;
564
  this.consecutiveWaiting = 0;
565
+ this.localFrameQueue = [];
566
  this.startResetBtn.textContent = 'Start';
567
  this.startResetBtn.classList.remove('btn-danger');
568
  this.startResetBtn.classList.add('btn-primary');