Skip to content

BodyRenderer is constructed per draw, so its private PathCache never hits #3

Description

@elmbg-sam

Summary

BodyRenderer holds its PathCache as an instance property:

// Sources/MuscleMap/Rendering/BodyRenderer.swift:62
private let pathCache = PathCache()

but a BodyRenderer is constructed fresh on every draw, so the cache is populated and thrown away each time and never serves a hit. Every render re-runs PathBuilder.buildPath over the full body — a few hundred SVG path strings — on the main thread.

Version: 1.6.4.

Where the renderer is rebuilt

BodyView.standardBody constructs one inside the Canvas draw closure:

// Sources/MuscleMap/Views/BodyView.swift
Canvas { context, size in
    makeRenderer(size: size).render(context: &context, size: size)
}

makeRenderer(size:) returns BodyRenderer(...), so each invalidation of that Canvas allocates a new renderer and a new empty PathCache. The same pattern appears in InteractiveBodyOverlay, BodyAccessibilityOverlay, MuscleTooltip and AnimatedBodyView, each with its own short-lived cache — so a single BodyView parses the body several times per frame.

Reproduction

struct ContentView: View {
    @State private var tick = 0

    var body: some View {
        VStack {
            // Any unrelated state change invalidates the Canvas and re-parses
            // the whole body.
            Text("\(tick)")
            BodyView(gender: .male, side: .front)
                .highlight(.chest, color: .red)
            Button("Redraw") { tick += 1 }
        }
    }
}

Add a counter to PathBuilder.buildPath (or a breakpoint) and it is hit the same number of times on every redraw, rather than only on the first.

The symptom in an app: a screen showing two bodies (front + back) is noticeably slow to appear in a Debug build and stutters while the surrounding view updates.

Suggestion

Make the cache outlive the renderer — a static let shared = PathCache(), or a cache owned by the view and passed in. The existing key already includes scale/offsetX/offsetY, so a shared instance stays correct across sizes; only eviction would need thought.

I worked around it downstream by wrapping the body in an EquatableView so it is only rebuilt when the highlight data actually changes, but that only reduces the frequency — the first render of each distinct state still re-parses everything.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions