Conversation
512eba5 to
47ddd7e
Compare
47ddd7e to
f47c3e8
Compare
T::Configuration.drop_unchecked_sigs!T::Utils.drop_unchecked_sigs!
A `sig` whose runtime checks never run still costs memory: sorbet-runtime keeps
its `Signature` in `@signatures_by_method` so that tools can introspect the
method. These are the `.checked(:never)` sigs, plus the `.checked(:tests)` sigs
outside a test environment. Those methods carry no wrapper at all, so nothing
but introspection reads those objects.
The new method frees them:
T::Utils.drop_unchecked_sigs!
It sweeps the registry and keeps no state: no flag, and no change to the
wrapping or dispatch paths. Later work records signatures again, both code that
loads after the call and the first call to a method reached through an alias, so
call the method again to free those too.
It sweeps rather than skipping the record at the source, because
`run_sig_block_for_key` reads the registry to serve a thread that lost the race
to run a `sig` block, including inside `run_all_sig_blocks`.
It lives on `T::Utils`, beside `run_all_sig_blocks`, `run_all_type_alias_blocks`
and `eagerly_define_all_lazy_props_methods!`, because it is a one-shot action at
preload time rather than configuration. Nothing can rebuild a freed signature,
so there is no way to undo the call.
The call trades introspection for memory:
- `T::Utils.signature_for_method` returns `nil` for those methods.
- `all_checked_tests_sigs` no longer lists them.
- Final method violations lose the "Made final here:" source location.
- A `sig` block that runs after the call cannot see a dropped parent signature,
so sorbet-runtime skips the override checks against that signature. Call the
method after the application loads all of its code, in the same way as
`T::Utils.run_all_sig_blocks`.
Signatures on `abstract` methods are never dropped. `Method#arity`,
`Method#source_location` and `Method#parameters` report the same values, because
those methods stay unwrapped.
For 10 000 methods with `.checked(:never)` sigs, the call frees about 7 MiB,
near 730 bytes per signature.
f47c3e8 to
bac9d73
Compare
jez
left a comment
There was a problem hiding this comment.
This mostly looks great to me, but I have some wording requests for the docs (thanks for updating the docs in the first place).
| end | ||
| end | ||
|
|
||
| unchecked = Class.new do |
There was a problem hiding this comment.
It doesn't seem like we need Class.new for these tests—the class could be declared like normal.
| This frees the signatures that `sorbet-runtime` holds already. There is no way back: nothing can rebuild a freed signature, because its `sig` block has already run. Signatures built after the call are recorded as usual: a `sig` block that runs later, either on the first call to its method or through `T::Utils.run_all_sig_blocks`, and the first call to a method through an alias. Call the method again to free those too: | ||
|
|
||
| ```ruby | ||
| T::Utils.drop_unchecked_sigs! | ||
|
|
||
| # … the application autoloads more code … | ||
|
|
||
| T::Utils.run_all_sig_blocks # evaluates the new sig blocks | ||
| T::Utils.drop_unchecked_sigs! # frees the ones that never check anything | ||
| ``` |
There was a problem hiding this comment.
The "call the method again" has me a little confused: when is the recommended time to call this method?
Most of the other methods, like run_all_sig_blocks, are meant to run after all autoloading has finished (e.g. eager loading). Why is the recommendation for this method different, such that you would want to call it both before and after eager loading?
| Runtime dispatch never uses these signatures, so calls behave the same. Methods with such sigs keep no wrapper at all, so `Method#arity`, `Method#source_location` and `Method#parameters` report the same values as before. The option only trades away introspection: | ||
|
|
||
| - `T::Utils.signature_for_method` returns `nil` for those methods. | ||
| - Final method violations lose the `Made final here:` source location. |
There was a problem hiding this comment.
I think we want to rewrite this a little, to make this warning more clear. Specifically, I think we want to call attention that the result of signature_for_method will be different in production before and after calling this method. This method makes it so that code cannot rely on signature_for_method producing a signature for all sig'd methods, only those whose signatures are checked.
A
sigwhose runtime checks never run still costs memory.sorbet-runtimekeeps everySignaturein@signatures_by_methodso that tools can introspect the method. These are the.checked(:never)sigs, plus the.checked(:tests)sigs outside a test environment. Such methods keep no wrapper at all, so nothing but introspection reads those objects.This PR adds the following public method for dropping those sigs that are unchecked:
This call frees the signatures
sorbet-runtimeholds already, and keeps no state at all. It touches neither the wrapping nor the dispatch path, so signatures built after the call are recorded as usual: anysigblock that runs later records the signature it derives back in the lookup table. Callingdrop_unchecked_sigs!a second time frees those too.The
drop_unchecked_sigs!clears signatures from the signature look up table rather than preventing them from being inserted in the first place. This is becauserun_sig_block_for_keyneeds to read the registry to serve a thread that lost the race to run asigblock. Skipping inserting the signature into the lookup table at the source would have turned that benign case into a raise, including insiderun_all_sig_blocksitself.Since the method keeps no state, it is on
T::Utils, next torun_all_sig_blocks,run_all_type_alias_blocksandeagerly_define_all_lazy_props_methods!. It is a one shot action at preload time, and not a configuration. There is also no way to undo it, because nothing can rebuild a freed signature.The drawbacks of calling
drop_unchecked_sigs!is documented inwebsite/docs/load-time-tuning.mdas the following:T::Utils.signature_for_methodwill returnnilfor those methods.Made final here:source location.sigblock that runs after the call cannot see a dropped parent signature, sosorbet-runtimeskips the override checks against it. Call it after the application has loaded all of its code, likeT::Utils.run_all_sig_blocks.Signatures on abstract methods are never dropped.
Motivation
At Shopify we run all
sigblocks after boot with checked levelnever. That unwraps every method eagerly, and the registry, and its associatedSignatureobjects, just become dead weight of about 70 megabytes.The alias path was the blocker for simply clearing the registry: an alias shares the first-call wrapper of the original and re-recorded its own signature on its first call. #10397 has already removed the registry from the alias dispatch path, so only the bookkeeping remained, and #10502 has made further progress on that front, so that this can become a method that needs no persistent state.
All we need at this point, is a safe way to clear the registry.
Test plan
See the included automated tests.