Skip to content

Commit b8d9b72

Browse files
committed
core: Add Scout-compatible FLM telemetry system
Add an opt-in telemetry backend that records frame, render, input, network, and memory metrics in Adobe Scout's FLM format, with support for live streaming over TCP in addition to writing to a file. - New `core/src/telemetry.rs` module with span/counter recording and AMF3-based FLM serialization - Desktop CLI gains a `--telemetry` flag to opt in and configure output - Player and context wiring to record frame/render/input/network spans and counters, plus bitmap decompression timing (`rend.buildbits`) - Adds a Python-based FLM reader/decoder and end-to-end tests under tests/tests/telemetry, plus usage documentation
1 parent 1bd08d6 commit b8d9b72

19 files changed

Lines changed: 3097 additions & 50 deletions

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ RECOVER_*.fla
1414
# Jetbrains tools (CLion, IntelliJ, etc)
1515
/.idea
1616

17+
# VS Code settings
18+
/.vscode
19+
1720
# Mac junk
1821
.DS_Store
1922

core/src/backend/audio.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,11 @@ pub trait AudioBackend: Any {
194194
fn is_sound_playing(&self, instance: SoundInstanceHandle) -> bool {
195195
self.get_sound_position(instance).is_some()
196196
}
197+
198+
/// Returns the total compressed memory used by all registered sounds, in kilobytes.
199+
fn total_sound_memory_kb(&self) -> i32 {
200+
0
201+
}
197202
}
198203

199204
/// Information about a sound provided to `NullAudioBackend`.
@@ -314,6 +319,10 @@ impl AudioBackend for NullAudioBackend {
314319
None
315320
}
316321

322+
fn total_sound_memory_kb(&self) -> i32 {
323+
(self.sounds.values().map(|s| s.size as usize).sum::<usize>() / 1024) as i32
324+
}
325+
317326
fn volume(&self) -> f32 {
318327
self.volume
319328
}

core/src/backend/audio/mixer.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -687,6 +687,10 @@ impl AudioMixer {
687687
self.sounds.get(sound).map(|s| s.data.len() as u32)
688688
}
689689

690+
pub fn total_sound_memory_kb(&self) -> i32 {
691+
(self.sounds.values().map(|s| s.data.len()).sum::<usize>() / 1024) as i32
692+
}
693+
690694
pub fn get_sound_format(&self, sound: SoundHandle) -> Option<&swf::SoundFormat> {
691695
self.sounds.get(sound).map(|s| &s.format)
692696
}
@@ -1167,5 +1171,10 @@ macro_rules! impl_audio_mixer_backend {
11671171
fn get_sample_history(&self) -> [[f32; 2]; 1024] {
11681172
self.$mixer.get_sample_history()
11691173
}
1174+
1175+
#[inline]
1176+
fn total_sound_memory_kb(&self) -> i32 {
1177+
self.$mixer.total_sound_memory_kb()
1178+
}
11701179
};
11711180
}

core/src/bitmap/bitmap_data.rs

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,17 @@ use ruffle_render::backend::RenderBackend;
88
use ruffle_render::bitmap::{
99
Bitmap, BitmapFormat, BitmapHandle, PixelRegion, PixelSnapping, SyncHandle,
1010
};
11+
use std::sync::atomic::{AtomicUsize, Ordering};
12+
13+
static BITMAP_MEMORY_BYTES: AtomicUsize = AtomicUsize::new(0);
14+
15+
/// Returns the current total bitmap memory in kilobytes,
16+
/// including both CPU-side BitmapData pixels and GPU-registered character bitmaps.
17+
pub fn bitmap_memory_kb() -> i32 {
18+
let cpu_bytes = BITMAP_MEMORY_BYTES.load(Ordering::Relaxed);
19+
let gpu_bytes = crate::character::registered_bitmap_memory_kb() as usize * 1024;
20+
((cpu_bytes + gpu_bytes) / 1024) as i32
21+
}
1122
use ruffle_wstr::WStr;
1223
use std::cell::Ref;
1324
use std::fmt::Debug;
@@ -417,6 +428,7 @@ pub enum DirtyState {
417428
}
418429

419430
mod wrapper {
431+
use super::BITMAP_MEMORY_BYTES;
420432
use crate::avm2::object::BitmapDataObject;
421433
use crate::context::RenderContext;
422434
use crate::display_object::DisplayObjectWeak;
@@ -427,6 +439,7 @@ mod wrapper {
427439
use ruffle_render::bitmap::{BitmapHandle, PixelRegion, PixelSnapping};
428440
use ruffle_render::commands::CommandHandler;
429441
use std::cell::Ref;
442+
use std::sync::atomic::Ordering;
430443

431444
use super::{BitmapRawData, DirtyState, copy_pixels_to_bitmapdata};
432445

@@ -494,9 +507,11 @@ mod wrapper {
494507
/// Clones the underlying data, producing a new `BitmapData`
495508
/// that has no GPU texture or associated display objects
496509
pub fn clone_data(&self, renderer: &mut dyn RenderBackend) -> BitmapRawData<'gc> {
497-
// Sync from the GPU to CPU, since our new BitmapData starts out
498-
// with no GPU texture
499510
let data = self.sync(renderer).borrow();
511+
BITMAP_MEMORY_BYTES.fetch_add(
512+
BitmapRawData::pixel_byte_size(data.width, data.height),
513+
Ordering::Relaxed,
514+
);
500515
BitmapRawData {
501516
pixels: data.pixels.clone(),
502517
width: data.width,
@@ -506,7 +521,6 @@ mod wrapper {
506521
bitmap_handle: None,
507522
avm2_object: None,
508523
display_objects: vec![],
509-
// We have no GPU texture, so there's no need to mark as dirty
510524
dirty_state: DirtyState::Clean,
511525
#[cfg(feature = "egui")]
512526
egui_texture: Default::default(),
@@ -728,7 +742,12 @@ impl std::fmt::Debug for BitmapRawData<'_> {
728742
}
729743

730744
impl<'gc> BitmapRawData<'gc> {
745+
fn pixel_byte_size(width: u32, height: u32) -> usize {
746+
width as usize * height as usize * std::mem::size_of::<Color>()
747+
}
748+
731749
pub fn new(width: u32, height: u32, transparency: bool, fill_color: u32) -> Self {
750+
BITMAP_MEMORY_BYTES.fetch_add(Self::pixel_byte_size(width, height), Ordering::Relaxed);
732751
Self {
733752
pixels: vec![
734753
Color::bgra_u32(fill_color).to_premultiplied_alpha(transparency);
@@ -753,6 +772,7 @@ impl<'gc> BitmapRawData<'gc> {
753772
transparency: bool,
754773
pixels: Vec<Color>,
755774
) -> Self {
775+
BITMAP_MEMORY_BYTES.fetch_add(Self::pixel_byte_size(width, height), Ordering::Relaxed);
756776
Self {
757777
pixels,
758778
width,
@@ -773,11 +793,14 @@ impl<'gc> BitmapRawData<'gc> {
773793
}
774794

775795
pub fn dispose(&mut self) {
796+
BITMAP_MEMORY_BYTES.fetch_sub(
797+
Self::pixel_byte_size(self.width, self.height),
798+
Ordering::Relaxed,
799+
);
776800
self.width = 0;
777801
self.height = 0;
778-
self.pixels = Vec::new(); // free the CPU pixel buffer
802+
self.pixels = Vec::new();
779803
self.bitmap_handle = None;
780-
// There's no longer a handle to update
781804
self.dirty_state = DirtyState::Clean;
782805
self.disposed = true;
783806
}

core/src/character.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use std::cell::OnceCell;
2+
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
23

34
use crate::backend::audio::SoundHandle;
45
use crate::binary_data::BinaryData;
@@ -14,6 +15,22 @@ use ruffle_render::bitmap::{Bitmap as RenderBitmap, BitmapHandle, BitmapSize};
1415
use ruffle_render::error::Error as RenderError;
1516
use swf::DefineBitsLossless;
1617

18+
static REGISTERED_BITMAP_BYTES: AtomicUsize = AtomicUsize::new(0);
19+
20+
/// Accumulates microseconds spent decompressing image assets (JPEG / lossless).
21+
/// Drained each frame into a telemetry span.
22+
static DECOMPRESS_MICROS: AtomicU64 = AtomicU64::new(0);
23+
24+
/// Returns the total GPU-registered bitmap memory (SWF character bitmaps) in kilobytes.
25+
pub fn registered_bitmap_memory_kb() -> i32 {
26+
(REGISTERED_BITMAP_BYTES.load(Ordering::Relaxed) / 1024) as i32
27+
}
28+
29+
/// Drains and returns the accumulated image-decompression time in microseconds since the last call.
30+
pub fn drain_decompress_micros() -> u64 {
31+
DECOMPRESS_MICROS.swap(0, Ordering::Relaxed)
32+
}
33+
1734
#[derive(Copy, Clone, Collect, Debug)]
1835
#[collect(no_drop)]
1936
pub enum Character<'gc> {
@@ -73,9 +90,13 @@ impl<'gc> BitmapCharacter<'gc> {
7390
if let Some(handle) = self.handle.get() {
7491
return Ok(handle.clone());
7592
}
93+
let t0 = std::time::Instant::now();
7694
let decoded = self.compressed.decode()?;
95+
DECOMPRESS_MICROS.fetch_add(t0.elapsed().as_micros() as u64, Ordering::Relaxed);
96+
let bitmap_bytes = decoded.width() as usize * decoded.height() as usize * 4;
7797
let new_handle = backend.register_bitmap(decoded)?;
7898
// FIXME - do we ever want to release this handle, to avoid taking up GPU memory?
99+
REGISTERED_BITMAP_BYTES.fetch_add(bitmap_bytes, Ordering::Relaxed);
79100
self.handle.set(new_handle.clone()).unwrap();
80101
Ok(new_handle)
81102
}

core/src/context.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ use crate::string::{AvmString, StringContext};
3737
use crate::stub::StubCollection;
3838
use crate::system_properties::SystemProperties;
3939
use crate::tag_utils::{SwfMovie, SwfSlice};
40+
use crate::telemetry::TelemetryMetrics;
4041
use crate::timer::Timers;
4142
use crate::vminterface::Instantiator;
4243
use async_channel::Sender;
@@ -73,6 +74,9 @@ pub struct UpdateContext<'gc> {
7374
/// A collection of stubs encountered during this movie.
7475
pub stub_tracker: &'gc mut StubCollection,
7576

77+
/// Telemetry metrics collector for the current SWF session.
78+
pub telemetry: &'gc mut TelemetryMetrics,
79+
7680
/// The library containing character definitions for this SWF.
7781
/// Used to instantiate a `DisplayObject` of a given ID.
7882
pub library: &'gc mut Library<'gc>,
@@ -355,6 +359,22 @@ impl<'gc> UpdateContext<'gc> {
355359
self.frame_rate,
356360
);
357361

362+
// Initialize telemetry with metadata from the newly-loaded SWF.
363+
let fps: f64 = movie.frame_rate().into();
364+
let size_kb = ((movie.compressed_len() + 512) / 1024).max(1) as i32;
365+
self.telemetry.set_swf_name(movie.url());
366+
self.telemetry.set_swf_rate(fps);
367+
self.telemetry.set_swf_size(size_kb);
368+
self.telemetry.set_swf_dimensions(
369+
movie.width().to_pixels() as i32,
370+
movie.height().to_pixels() as i32,
371+
);
372+
self.telemetry
373+
.set_swf_vm(if movie.is_action_script_3() { 2 } else { 1 });
374+
self.telemetry.add_network_loadmovie(movie.url());
375+
self.telemetry.add_network_received(size_kb);
376+
self.telemetry.set_swf_start(0);
377+
358378
*self.root_swf = Arc::new(movie);
359379
*self.instance_counter = 0;
360380

core/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ pub mod debug_ui;
5858
pub mod external;
5959
pub mod i18n;
6060
pub mod stub;
61+
pub mod telemetry;
6162

6263
pub use context_menu::ContextMenuItem;
6364
pub use events::PlayerEvent;

0 commit comments

Comments
 (0)