Create RBS::Location lazily, like Prism (prototype: -14.5% retained memory)
Maintainer thường phản hồi trong vòng 1 ngày
Chưa có ai nhận issue này.
Đánh giá
- Độ khó
- 5/5
- Thời gian dự kiến
- Hơn một tuần
- Mức phù hợp với người mới
- 22/100
- Loại issue
- Tái cấu trúc
- Độ rõ ràng
- Khá rõ ràng
- Mức độ hoạt động
- Sôi nổi
- Lĩnh vực
- performance, tooling
Hướng nghiên cứu
This is a design discussion, not a starter patch: read the prototype at github.com/ruby/rbs/compare/master...ksss:rbs:lazy-location-prototype, especially ext/rbs_extension/location_table.c, templates/ext/rbs_extension/ast_translation.c.erb, and lib/rbs/lazy_location.rb. Reproduce numbers with benchmark/memory_new_rails_env.rb and benchmark/memory_new_env.rb. Done means maintainers have answered the three questions (lazy locations vs Prism, Integer @location plus buffer:, map_type_name yield) and a follow-up implementation issue or PR is agreed—not implementing the full table-backed design from this thread alone.
Do mô hình lập chỉ mục viết ra từ nội dung của issue.
Mô tả
Summary
RBS::Location objects take 22–28% of the memory retained by a loaded RBS::Environment, but almost none of them are ever read. I prototyped creating them lazily, following Prism's approach. In benchmark/memory_new_rails_env.rb, retained memory went down by 14.5% (-8.2 MB) and every location stays identical to master. This issue shares the numbers and the open design questions, to decide whether this direction is worth pursuing.
Motivation
The parser creates one RBS::Location per node, eagerly, and the node keeps it for its whole lifetime.
- Locations are 2.38 MB of the 10.83 MB retained by
benchmark/memory_new_env.rb(22%), and 15.76 MB of 56.66 MB inbenchmark/memory_new_rails_env.rb(28%). - They are almost never read. With
core+ everystdliblibrary loaded, 60,734 Locations are created. I counted the ones whose contents were inspected (start_pos,[],to_s, ...):- after
Environment.from_loader(loader).resolve_type_names: 0 - after also building every instance/singleton/interface definition with
DefinitionBuilder: 2 (both for an error message)
- after
#location is called often, but mostly to pass the location to a rebuilt node (location: location in map_type_name, Environment#resolve_*, ...), not to look at it.
Prism does this already
The current parser follows Prism's design, and Prism (1.9) creates locations lazily:
# prism/lib/prism/node.rb
def location
location = @location
return location if location.is_a?(Location)
@location = Location.new(source, location >> 32, location & 0xFFFFFFFF)
end
The C extension stores (start << 32) | length as an Integer, so no object is allocated until #location is called (except with freeze: true, where locations are created eagerly). Every node also keeps @source, and sub-locations like keyword_loc are separate node fields, each lazy in the same way.
What the prototype does
1. Nodes without child locations (Types::Bases::*, Optional, Union, Tuple, Record, Literal, Variable, Proc, Block, AST::Comment, AST::Annotation, Members::Public/Private, ...)
- The C extension passes
location: (start_char << 32) | lengthandbuffer: bufferto.new. #locationbuilds theRBS::Locationon first call and replaces the ivar.
2. Nodes with child locations (ClassInstance, Alias, Interface, Function::Param, MethodType, TypeParam, declarations, members, directives)
- An Integer can hold only one range, so the child ranges go into a per-buffer table: a single
int32array attached to theBuffer(a CRBS::LocationTable). Each record is[node_type, start, end, (child_start, child_end) * N]. - The node keeps the record index in
@locationand the buffer in@buffer. - Child names (
:keyword,:name, ...) are fixed per node type, so they come from a static schema generated fromconfig.ymland are not stored per location. #locationbuilds the sameRBS::Location, with the same children, as the parser builds today.
3. Pass-through sites that rebuild a node of the same class (map_type, map_type_name, sub, update, Environment#resolve_*) pass the raw @location / @buffer along, so they don't materialize anything.
RBS::AST::Ruby::* (inline annotations) and the WASM path are left unchanged.
Results
ruby 4.0.6 / arm64-darwin, master 29b5984c. Both columns come from the same checkout; only the sources were switched and the extension rebuilt. Numbers are from memory_profiler.
benchmark/memory_new_rails_env.rb:
| master | prototype | ||
|---|---|---|---|
| Total retained | 56.66 MB / 663,335 objects | 48.45 MB / 503,125 objects | -14.5% / -24.2% |
Retained RBS::Location |
161,671 | 1,178 | -99.3% |
| Total allocated | 90.67 MB / 1,232,424 objects | 93.12 MB / 1,221,053 objects | +2.7% / -0.9% |
benchmark/memory_new_env.rb (core only):
| master | prototype | ||
|---|---|---|---|
| Total retained | 10.83 MB / 99,116 objects | 9.57 MB / 74,314 objects | -11.7% / -25.0% |
Retained RBS::Location |
24,917 | 28 |
Where the retained memory goes in the Rails case:
RBS::Location objects removed |
-15.62 MB |
RBS::LocationTable added |
+3.58 MB |
Nodes that move to a larger GC slot because of the extra @buffer ivar |
+3.83 MB |
↳ Members::MethodDefinition (8 → 9 ivars, 80 → 160 bytes) |
+1.62 MB |
↳ Types::Function::Param (3 → 4 ivars, 40 → 80 bytes) |
+0.97 MB |
↳ Types::ClassInstance (3 → 4 ivars, 40 → 80 bytes) |
+0.88 MB |
↳ others (Types::Alias, AttrReader, Class::Super, ...) |
+0.36 MB |
| Net | -8.21 MB |
Step 1 alone (nodes without child locations) gives -3.53 MB (-6.2%) in the Rails case. None of those nodes cross a slot boundary, because they have at most two ivars before @buffer is added.
CPU time was not measured.
Correctness
- Parsing every file in
coreandstdliband dumping each node's location (range, plus the name, required/optional flag and range of every child) gives the same 60,280 lines with master and with the prototype. rake testpasses exceptRBS::WASM::SerializationTest(4 errors). That test compares the ivars of the C and WASM results directly, and the WASM deserializer was not updated.
Concerns and open questions
-
@locationcan be an Integer.#locationstill returns anRBS::Location, but code that reads the ivar directly (instance_variable_get, Marshal, subclasses) sees an Integer. -
Constructors get a
buffer:keyword, and code that rebuilds a node must pass@location/@bufferinstead oflocationto keep the savings. Code outside rbs that doesFoo.new(..., location: other.location)still works, but materializes the location. -
map_type_nameyields the location as its second block argument (yield(name, location, self)), which materializes it for every type. Every block inlib/ignores it (|name, _, _|). The prototype checksblock.parametersand only materializes when that parameter is named. That keeps the API but allocates on every call, which is where the +2.7% allocated bytes comes from. Options: deprecate the argument, or yield something cheaper. -
Slot growth. Adding
@bufferpushes some nodes into a larger GC slot (+3.83 MB above). Prism nodes always carry@source,@node_idand@flags, so the extra ivar costs little there; RBS nodes are smaller, so it shows. Possible ways out: keep the buffer reference somewhere other than an ivar for those classes, or reduce the ivar count ofMethodDefinition. -
Table size. The table stores absolute
int32positions. Storing child ranges asuint16offsets from the node's start would roughly halve its 3.58 MB. -
Location identity. Today a node and the copy made by
map_type_nameshare oneLocationobject. With lazy creation, each materializes its own, soa.location.equal?(b.location)no longer holds, and materializing both costs two objects. -
Frozen / Ractor-shareable ASTs. Lazy creation writes the ivar on first access, so it doesn't work with frozen nodes. Prism creates locations eagerly when
freeze: true. RBS would need the same if it ever shares ASTs across Ractors. -
WASM / JRuby path.
lib/rbs/wasm/deserializer.rbbuilds nodes in Ruby and would need the same representation (or keep eager locations). -
Signatures.
sig/needs@location: Location | Integer, thebuffer:keyword, and so on.
Lifetimes don't change: a Location already keeps its Buffer alive, and now the node keeps it through @buffer. The table is attached to the Buffer, so it is freed together with it.
Possible plan
- Nodes without child locations only. Small API surface, no slot growth, -6.2% retained memory in the Rails benchmark.
- Table-backed locations for nodes with child locations. Around -14.5% in total, with concerns 3 and 4 to settle.
- Reduce the costs: slot growth, table compaction.
Questions:
- Is aligning
RBS::Locationwith Prism's lazy locations the direction you want? - Is it acceptable for
@locationto hold an Integer, and to add abuffer:keyword to node constructors? - For
map_type_name, would you rather deprecate the location block argument, or keep it as is?
The prototype is three WIP commits: https://github.com/ruby/rbs/compare/master...ksss:rbs:lazy-location-prototype
The C side is ext/rbs_extension/location_table.c and the template changes in templates/ext/rbs_extension/ast_translation.c.erb; the Ruby side is lib/rbs/lazy_location.rb plus mechanical changes to the node classes.
Related: #3200 (sharing the frozen empty array for empty node lists), another memory reduction for retained ASTs.
- Ngôn ngữ chính
- Ruby
- Star
- 2.2k
- Fork
- 258
- Merge trung bình
- 1 ngày 4 giờ
- Pull request đã merge (30 ngày)
- 52
Chuẩn bị môi trường
- Không có Dockerfile hay tệp Docker Compose
- Không có mẫu pull request
- Đọc hướng dẫn đóng góp
Bắt đầu từ đâu
- Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
- Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
- Fork repository và làm thay đổi trên một nhánh.
- Mở pull request có tham chiếu số hiệu của issue.
Issue khác của ruby/rbs
-
InlineParser: `@rbs @ivar` annotation is reported as unused unless it is the first thing in the class bodyCó thể đã có người làm @ksss đã nhận 15 ngày trước. Đang mở
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 82/100
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 85/100
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 1/5 Dưới một giờ Mức phù hợp với người mới 78/100
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 72/100
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 4/5 3-5 ngày Mức phù hợp với người mới 57/100
Maintainer thường phản hồi trong vòng 1 ngày
Issue tương tự
-
bug
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 82/100
Maintainer thường phản hồi trong vòng 1 ngày
-
iOS podspec depends on `React-Codegen`, which resolves to an unrelated pod on CocoaPods trunkĐang mở
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 75/100
GantMan/jail-monkey#380 ·
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 62/100
voxpupuli/puppet-wireguard#151 ·
-
bug
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 72/100
Maintainer thường phản hồi trong vòng 1 ngày
-
Search accepts any value for `requests_per_page` and `per_page`Có thể đã có người làm Có pull request liên kết đang mở hoặc đã được merge. Đang mởbug f:search
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 78/100
Maintainer thường phản hồi trong vòng 1 ngày