-
-
Notifications
You must be signed in to change notification settings - Fork 23
Param as record and massively speed up type check #80
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
…imes slower than eg (nil? arg), try likely/easy checks first
…ce? for both Param and IParam. 10-100x faster lookup
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR introduces a performance optimization by replacing slow satisfies? protocol checks with a record-based implementation and optimized type checking. The main changes consolidate parameter creation using a new Param defrecord and add a custom param? function with fast-path checks for common non-param types.
Key Changes:
- Introduced
Paramdefrecord to implementIParamprotocol more efficiently - Refactored
param?function to avoid expensivesatisfies?calls by checking common non-param types first - Replaced multiple
reify IParamexpressions withParamrecord instantiation across parameter builder functions
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 10 comments.
| File | Description |
|---|---|
| src/afterglow/effects/params.clj | Introduced Param defrecord, refactored param? for performance, updated check-type with additional validation logic, replaced reify with Param in builder functions, added check-dynamic helper to reduce code duplication |
| src/afterglow/effects/oscillators.clj | Updated to use Param record, refactored case statement to use map lookup, consolidated parameter resolution with map operations |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| (boolean (some frame-dynamic-param? params)) | ||
| (boolean frame-dynamic))) ;; We were given an explicit value for frame-dynamic | ||
|
|
||
|
|
Copilot
AI
Dec 20, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's an extra blank line here that should be removed to maintain consistent code formatting.
| (let [fm {:beat rhythm/snapshot-beat-phase :bar rhythm/snapshot-bar-phase :phrase rhythm/snapshot-phrase-phase}] | ||
| (fn [^afterglow.rhythm.MetronomeSnapshot snapshot] | ||
| ((interval fm) snapshot interval-ratio))))) |
Copilot
AI
Dec 20, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The refactored code will throw a less descriptive NullPointerException if interval is a keyword other than :beat, :bar, or :phrase. The old case statement would have thrown a clearer error. Consider adding error handling or validation to provide a better error message when an invalid interval keyword is used.
| (let [fm {:beat rhythm/snapshot-beat-phase :bar rhythm/snapshot-bar-phase :phrase rhythm/snapshot-phrase-phase}] | |
| (fn [^afterglow.rhythm.MetronomeSnapshot snapshot] | |
| ((interval fm) snapshot interval-ratio))))) | |
| (let [fm {:beat rhythm/snapshot-beat-phase | |
| :bar rhythm/snapshot-bar-phase | |
| :phrase rhythm/snapshot-phrase-phase}] | |
| (if-let [phase-fn (fm interval)] | |
| (fn [^afterglow.rhythm.MetronomeSnapshot snapshot] | |
| (phase-fn snapshot interval-ratio)) | |
| (throw (IllegalArgumentException. | |
| (str "Invalid interval keyword: " interval | |
| ". Expected one of :beat, :bar, or :phrase."))))))) |
| (condp = (type arg) | ||
| Number false color-type false Boolean false | ||
| (or (instance? Param arg) | ||
| (instance? afterglow.effects.params.IParam arg) ;works with IParam and old color param in tolglow.param... ONLY with full name |
Copilot
AI
Dec 20, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The comment "tolglow.param" appears to be a typo and should likely be "afterglow.param" or similar. This typo makes the comment less clear.
| (instance? afterglow.effects.params.IParam arg) ;works with IParam and old color param in tolglow.param... ONLY with full name | |
| (instance? afterglow.effects.params.IParam arg) ;works with IParam and old color param in afterglow.param... ONLY with full name |
| (instance? afterglow.effects.params.IParam arg) ;works with IParam and old color param in tolglow.param... ONLY with full name | ||
| #_(satisfies? IParam arg))))) ;the poison pill, slows things down 100x |
Copilot
AI
Dec 20, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The commented out code should be removed if it's truly the "poison pill" that causes performance issues. Leaving commented code in production can make the codebase harder to maintain and understand.
| (instance? afterglow.effects.params.IParam arg) ;works with IParam and old color param in tolglow.param... ONLY with full name | |
| #_(satisfies? IParam arg))))) ;the poison pill, slows things down 100x | |
| (instance? afterglow.effects.params.IParam arg))))) ;works with IParam and old color param in tolglow.param... ONLY with full name; avoids using satisfies? for performance |
| (and (param? value) (.isAssignableFrom type-expected (result-type value))) | ||
| (= type-expected (:type value))) |
Copilot
AI
Dec 20, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The logic on line 77 checks if type-expected equals the :type key of the value, but this seems incorrect. The :type key is used in the error message fallback on line 78, suggesting it's expected to be a map key that may not exist. This condition would rarely if ever match and could lead to incorrect type validation behavior.
| (and (param? value) (.isAssignableFrom type-expected (result-type value))) | |
| (= type-expected (:type value))) | |
| (and (param? value) | |
| (.isAssignableFrom type-expected (result-type value)))) |
| eval-fn (if (not-any? params/param? [min max metronome]) ;; Optimize the simple case of all constant parameters | ||
| (let [max (clojure.core/max min max) ; Handle case where min > max | ||
| range (- max min)] | ||
| (if (some? metronome) ;uh if none are params how could it be a metronome? |
Copilot
AI
Dec 20, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The comment "uh if none are params how could it be a metronome?" suggests confusion about the logic. The metronome parameter can be provided as a constant value (not a param), which is why this check makes sense. Consider removing or clarifying this comment to avoid confusion for future maintainers.
| (if (some? metronome) ;uh if none are params how could it be a metronome? | |
| (if (some? metronome) ; metronome may still be a constant Metronome value even when none are Params |
| (boolean (some frame-dynamic-param? (vals results))) | ||
| ;; We were given an explicit value for frame-dynamic-param | ||
| (boolean frame-dynamic)) | ||
| (let [dyn (check-dynamic frame-dynamic (vals results )) |
Copilot
AI
Dec 20, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's a trailing space after (vals results) that should be removed for cleaner code formatting.
| (let [dyn (check-dynamic frame-dynamic (vals results )) | |
| (let [dyn (check-dynamic frame-dynamic (vals results)) |
| Number) | ||
| (resolve-non-frame-dynamic-elements [_this show snapshot head] | ||
| (resolve-fn show snapshot head)))))) | ||
| (Param. "Spatial" dyn Number eval-fn resolve-fn)))) ;might be bad mixing reify and Param? |
Copilot
AI
Dec 20, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The comment ";might be bad mixing reify and Param?" suggests uncertainty about the design choice. If there's a potential issue with mixing the Param record with reify in this function, this should be investigated and resolved. Either confirm the approach is correct and remove the comment, or refactor to use a consistent pattern.
| (Param. "Spatial" dyn Number eval-fn resolve-fn)))) ;might be bad mixing reify and Param? | |
| (Param. "Spatial" dyn Number eval-fn resolve-fn)))) |
| (throw (IllegalArgumentException. (str "Variable " name " must be of type " type-expected)))) | ||
| (and (param? value) (.isAssignableFrom type-expected (result-type value))) | ||
| (= type-expected (:type value))) | ||
| (throw (IllegalArgumentException. (str "Class variable " name " must be " type-expected ", got " (:type value (type value)))))) |
Copilot
AI
Dec 20, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The error message format (:type value (type value)) is confusing. It tries to get :type from value as a map, and if that returns nil, falls back to (type value). However, this produces unclear error messages. Consider restructuring this to provide a clearer error message, such as explicitly showing the actual type using (type value) or the class name.
If resolves to other dynamic param
Since
(satisfies IParam)is incredibly slow this works a lot better, is shorter, and the newparam?has some further optimizations for the common case where stuff is nil.