Repository navigation
perf: drop FFI calls slower than their fallback, zero clippy warnings, incremental Birdseye redraw - #295
Open
manupawickramasinghe wants to merge 7 commits into
Open
perf: drop FFI calls slower than their fallback, zero clippy warnings, incremental Birdseye redraw#295manupawickramasinghe wants to merge 7 commits into
manupawickramasinghe wants to merge 7 commits into
Conversation
Measured each Rust-dispatching helper against its Python/OpenCV fallback on realistic per-frame inputs (see PR for the full table): - intersection_over_union, intersects_any, inside_any, average_boxes and median_of_boxes were 1.5x-7x slower through ctypes than in Python, and the Rust IoU worked in f32 so it did not reproduce the f64 result. - create_tensor_input's fused Rust crop/convert/resize took ~1.0 ms for a 320x320 detector input versus ~0.18 ms for the OpenCV path, and used full-range BT.601 coefficients, so the detector saw a lower-contrast image (video white Y=235 became 235 instead of 255) than without Rust. These now always use the Python/OpenCV implementation. Remove FFI exports and bindings that nothing calls: point_in_polygon, polygon_box_overlap, fast_shm_copy, preprocess_detect_input, yuv420_to_rgb, nhwc_to_nchw_f32 and the now-unused intersection_over_union, intersects_any_i32, inside_any_i32, average_and_median_boxes_i32 and yuv420_region_to_rgb_resize (frame-rs); motion_detect_full, motion_detect, motion_init_average and motion_accumulate_weighted plus the kernels only they used (motion-rs). Zone checks keep cv2.pointPolygonTest: it takes ~0.25 us per call versus ~1.8 us for the Rust binding (0.4 us even with a pre-built ctypes array). In motion-rs, rename dilate_3x3 to dilate_cross_3x3 and document that it and the rest of motion_pixel_pipeline approximate, rather than reproduce, the OpenCV fallback. Behavior is unchanged. Add frigate/test/test_box_helpers.py pinning the helpers' results and create_tensor_input's equality with the OpenCV conversion. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Formatting only, no code changes. Kept separate so the following commits' diffs are reviewable. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Add # Safety sections to every pub unsafe extern "C" fn that lacked one, stating the pointer validity and buffer sizes each relies on and what the null/zero guards do. - frame-rs: calculate_region_i32 takes (frame_h, frame_w) and the bounds as a tuple/array (was 8 args); fix the yuv420_crop_repack doc, which said the chroma output is interleaved when it is planar I420; make the crate doc an inner //! comment. - yolo-rs: decode_yolo_scale appends into a Candidates struct instead of returning a 3-tuple of Vecs (too_many_arguments, type_complexity, and three fresh 1024-entry Vecs per scale); iterate anchors and DFL sides with iterators (same arithmetic order). - detector-rs: drop the never-constructed Msg::Shutdown variant (the threads stop when the channel closes), use io::Error::other and is_multiple_of, drop a same-type cast, and make the f16 conversion test compare bits exactly; its EPSILON tolerance accepted any value below 1.2e-7, so it could not catch a wrong subnormal. - Tests: useless_vec, identity_op and needless_range_loop fixes. cargo clippy --release --all-targets: 77 warnings before this series, 0 after. No allow attributes were needed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every composed Birdseye frame re-resized all five YUV planes of every camera in the layout, although only the camera that sent the frame (or a few, given the 10 fps output cap) had changed. Tiles only move when the layout is reset, and that path clears the canvas, so a camera whose frame has not changed since it was drawn is already correct. Track a per-camera frame sequence number and the one last painted; skip the copy when they match and forget them on clear_frame(). Also remove the unused "layout_frame" key. testing-scripts/benchmark_birdseye.py (8 x 1280x720 cameras at 5 fps): composing update: 2267-2870 us -> 381-413 us 40 frames/s in, 10 fps out: 46-50 ms CPU/s -> 28-29 ms CPU/s The resulting canvas is byte-identical (same checksum). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
update() runs for every tracked object on every frame and built its debug messages with f-strings whether or not debug logging was enabled. Switch to %-style arguments as the project logging guidelines ask, and only build the speed-zone message, which runs several numpy calls (~2 us) to format the pixel velocity, when debug is enabled. Use the loop's zone object instead of looking the zone up again by name. No measurable change for a normal object (11.6 us per update before and after, 4 zones); this is for consistency and for speed zones. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
rust_yolo re-assigned argtypes/restype on every call, unlike frame_rs and rust_engine. That repeats work per inference and races when two threads call the same function. Configure all four prototypes when the library is loaded (a library missing a symbol is now treated as unavailable instead of failing on first use) and drop two redundant function-local numpy imports. yolo26_post_process (2100 cells): 105.0 -> 97.9 us; nms_boxes (50): 8.8 -> 8.4 us. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
test_transform_detect_input_and_normalize_parity calls the Rust kernels directly and test_yolo26_and_anchor_free_and_nms_post_process feeds tensor layouts only the Rust post-processors handle, so both errored instead of skipping in an environment without the libraries. test_rust_libraries_loaded still fails there on purpose. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Performance and code-quality pass over the Python per-frame paths and the four Rust crates. All the numbers below come from running the code in the
frigate-ncnn-vulkan-npuimage on the dev host. Frigate was running on the same host, so expect some noise, but every gain here is well beyond it.Summary
cargo fmtcommit.allowattributes. Everypub unsafe extern "C"fn now has a# Safetysection.TrackedObject.update,rust_yoloprototypes declared once, and two Rust-only tests now skip when Rust is missing.1. Rust dispatch that was slower than the fallback
I timed each helper that dispatches to Rust against its own fallback, with per-frame-sized inputs (
minof 5 repeats):intersection_over_union(1 pair)intersects_any(3 / 20 boxes)inside_any(3 / 20 boxes)average_boxes(10)median_of_boxes(10)create_tensor_inputrgb 320→320create_tensor_inputrgb 640→320point_in_polygon(never wired)cv2.pointPolygonTest0.25 µsThese helpers now always use the Python/OpenCV implementation. The Rust kernels and bindings behind them are removed.
create_tensor_inputis the one that matters most. It runs for every detection region on every frame, and the fused Rust crop+convert+resize took 5x longer thanyuv_crop_and_resize+cv2.cvtColor+cv2.resize. It also used full-range BT.601 coefficients, while OpenCV'sCOLOR_YUV2RGB_I420uses video-range. As a result, the detector saw a lower-contrast image whenever Rust was present: video white (Y=235) came out as 235 instead of 255, and black (Y=16) as 16 instead of 0.frigate/test/test_box_helpers.pynow checks thatcreate_tensor_inputmatches the OpenCV conversion and maps video-range white and black to 255 and 0. 14 of its checks fail against the old code.I did not wire the zone
point_in_polygon/polygon_box_overlapbindings intotracked_object.py.cv2.pointPolygonTestis already 7x faster than the binding, and the Rust ray-cast does not count points on an edge as inside, whilepointPolygonTest(...) >= 0does.Kept, because Rust is faster and gives the same result:
reduce_boxes(7.1 vs 15.0 µs),get_cluster_candidates(7.4 vs 60.9 µs; same clusters, deterministic order), the norfairdistance(2.1 vs 21.5 µs) and its 5x5 matrix (6.6 vs 560 µs),percentile_15_85(4.3 vs 110 µs),ncc_u8(16 vs 52 µs),yuv_crop_and_resize(38 vs 42 µs),yuv_to_3_channel_yuv(158 vs 213 µs),reduce_detections(it scales better: 27 vs 62 µs at 25 detections, and is even at small counts).Exports removed because nothing calls them:
point_in_polygon,polygon_box_overlap,fast_shm_copy(only a throughput test used it; it was a plainmemmove),preprocess_detect_input,yuv420_to_rgbandnhwc_to_nchw_f32in frame-rs. In motion-rs:motion_detect_full,motion_detect,motion_init_average,motion_accumulate_weighted, plus the kernels only they used (contrast stretch, histogram percentile, masked average update, accumulate).get_anchor_free_grid_pointsis still used byanchor_free_post_process, so it stays.2. Clippy and fmt
These are unique
cargo clippy --release --all-targetswarnings. About half disappeared with the dead code. For the rest:# Safetysections now state the actual contract: buffer lengths in elements, aliasing, and what the null/zero guards do.allow(clippy::too_many_arguments)was needed:calculate_region_i32takes a frame tuple and a bounds array.decode_yolo_scaleappends into aCandidatesstruct instead of returning a 3-tuple of fresh 1024-capacity Vecs per scale.Msg::Shutdown; the threads already stop when the channel closes.io::Error::otherandis_multiple_of.abs() <= f32::EPSILONtolerance accepted any value below 1.2e-7, so it could not catch a wrong subnormal.//!comments, and theyuv420_crop_repackdoc no longer says the chroma output is interleaved (it is planar I420).cargo fmtis in its own commit (e1cccb0a4, formatting only) so it can be skipped when reviewing.3. Birdseye: redraw only changed cameras
Each composed frame used to resize all five YUV planes of every camera in the layout, even though usually only one camera had a new frame. Tiles only move when the layout is reset, and that path clears the canvas. Each camera now carries a frame sequence number and the number last painted. The copy is skipped when they match, and both are forgotten on
clear_frame().Measured with
testing-scripts/benchmark_birdseye.py(added; 8 cameras at 1280x720, 5 fps each):update()The canvas checksum is identical.
frigate/test/test_birdseye_redraw.pychecks three things: an unchanged camera is not redrawn, the incrementally built canvas equals a full redraw, and a layout reset redraws everything.4. Smaller changes
TrackedObject.updatenow uses %-style lazy logging, as the project's logging guidelines ask. The speed-zone debug message, which runs ~2 µs of numpy to format the velocity, is only built when debug logging is on. It also uses the loop'szoneinstead of looking the zone up again by name. There is no measurable change for a normal object (11.6 µs per update with 4 zones, before and after), so this is consistency, not a speed claim.rust_yolonow declares its ctypes prototypes once at load, likeframe_rsandrust_enginealready do. Previouslyargtypeswas re-assigned on every call, which raced across threads. A library missing a symbol is now treated as unavailable. Timings:yolo26_post_process(2100 cells) 105.0 → 97.9 µs,nms_boxes(50) 8.8 → 8.4 µs.box_insidehad its condition duplicated; it is now a single expression. Type hints were added to the box helpers touched here.test_transform_detect_input_and_normalize_parity,test_yolo26_and_anchor_free_and_nms_post_process) now skip instead of erroring when the libraries are missing.Test results
/opt/frigate):Ran 1004 tests,OK (skipped=1). Baselinedevwas 991 tests: this PR adds 14 and removes 1 (thefast_shm_copythroughput test)./opt/frigate/libfrigate_*_rs.so, and no.soanywhere on the search path):Ran 1004 tests,FAILED (failures=1, skipped=5). The only failure istest_rust_libraries_loaded, which by design asserts that the libraries load. The newtest_box_helpersandtest_birdseye_redrawpass both ways (14/14).cargo test --release: frame 22, motion 15, yolo 12, detector 2, all passing.cargo fmt --checkis clean.ruff check frigate migrations docker *.pyreports "All checks passed!", andruff format --checkreports 403 files already formatted.Not changed, and why
The motion-rs pixel pipeline is not equivalent to the OpenCV fallback, and I left its behaviour alone. I renamed
dilate_3x3todilate_cross_3x3and documented the differences onmotion_pixel_pipeline:[1 2 1]/4blur instead ofgaussian_filter(sigma=1);>=instead of>for the threshold;area >= mininstead of 8-connectedfindContours/contourAreawitharea > min.On a synthetic 720p scene (noise plus three moving rectangles, 270 frames after calibration), the two paths gave identical box lists on only 3% of frames, with an average of 38.6 vs 8.0 boxes per frame. Making Rust match OpenCV would change motion results in production, so it needs its own PR and a decision. Open question: should the Rust path be made to match the OpenCV fallback, or the other way round?
transform_detect_input/normalize_u8_to_f32dispatch is kept. Rust is about even with numpy (976 vs 918 µs NCHW 320, 80 vs 108 µs NHWC 320). It returns a contiguous NCHW tensor where numpy returns a strided view-copy, so the net effect depends on the detector.Untouched:
frigate/embeddings/onnx/face_embedding.pyand the face kernels (bilinear_resize_u8,laplacian_variance_u8,batch_cosine_similarity_f32),read_ffmpeg_frame, VAAPI/hwaccel, andtest/upstream-0.18-rust-integration/ feat: sync upstream 0.18 beta with custom Rust accelerators & security patches #244.detector-rs only post-processes YOLO26; other architectures return 0 detections. The Python side only spawns it for
arch == "yolo26", so I left it.The Python post-processing fallback (
frigate/util/model.py) can't handle the anchor-free tensor layouts that only the Rust path supports. That's why the second parity test above skips without Rust. Adding a fallback is a separate change.🤖 Generated with Claude Code