tg_geometry 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +103 -0
  3. data/Gemfile +3 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +385 -0
  6. data/Rakefile +129 -0
  7. data/benchmark/_support.rb +115 -0
  8. data/benchmark/batch_packed_vs_loop.rb +27 -0
  9. data/benchmark/falcon_concurrency.rb +25 -0
  10. data/benchmark/flat_vs_rtree.rb +27 -0
  11. data/benchmark/gvl_threshold.rb +41 -0
  12. data/benchmark/objectspace_memsize.rb +17 -0
  13. data/benchmark/parse_throughput.rb +38 -0
  14. data/benchmark/rss_stability.rb +70 -0
  15. data/docs/ACTIVE_RECORD.md +26 -0
  16. data/docs/ARCHITECTURE.md +130 -0
  17. data/docs/AUTO_STRATEGY.md +15 -0
  18. data/docs/BENCHMARKING.md +75 -0
  19. data/docs/CASUAL_EXAMPLE.md +618 -0
  20. data/docs/CONCURRENCY.md +65 -0
  21. data/docs/ERROR_HANDLING.md +55 -0
  22. data/docs/EXPANSION_E_TO_H_STATUS.md +51 -0
  23. data/docs/FORMAT_COVERAGE.md +23 -0
  24. data/docs/FULL_TG_API_COVERAGE.md +109 -0
  25. data/docs/LIMITATIONS.md +61 -0
  26. data/docs/LOW_LEVEL_GEOMETRY.md +121 -0
  27. data/docs/MEMORY_OWNERSHIP.md +94 -0
  28. data/docs/RACTOR.md +40 -0
  29. data/docs/REGISTRY.md +37 -0
  30. data/docs/RELEASE_CHECKLIST.md +39 -0
  31. data/ext/tg_geometry/extconf.rb +91 -0
  32. data/ext/tg_geometry/tg_geometry_ext.c +3054 -0
  33. data/ext/tg_geometry/tg_geometry_vendor_rtree.c +1 -0
  34. data/ext/tg_geometry/tg_geometry_vendor_tg.c +24 -0
  35. data/ext/tg_geometry/vendor/.vendored +16 -0
  36. data/ext/tg_geometry/vendor/rtree/LICENSE +20 -0
  37. data/ext/tg_geometry/vendor/rtree/README.md +202 -0
  38. data/ext/tg_geometry/vendor/rtree/VERSION +3 -0
  39. data/ext/tg_geometry/vendor/rtree/rtree.c +840 -0
  40. data/ext/tg_geometry/vendor/rtree/rtree.h +105 -0
  41. data/ext/tg_geometry/vendor/tg/LICENSE +19 -0
  42. data/ext/tg_geometry/vendor/tg/README.md +197 -0
  43. data/ext/tg_geometry/vendor/tg/VERSION +3 -0
  44. data/ext/tg_geometry/vendor/tg/tg.c +16010 -0
  45. data/ext/tg_geometry/vendor/tg/tg.h +359 -0
  46. data/lib/tg/geometry/active_record_source.rb +57 -0
  47. data/lib/tg/geometry/registry.rb +119 -0
  48. data/lib/tg/geometry/version.rb +7 -0
  49. data/lib/tg/geometry.rb +6 -0
  50. data/lib/tg_geometry.rb +3 -0
  51. data/script/vendor_libs.rb +264 -0
  52. data/spec/block_10_rtree_strategy_spec.rb +82 -0
  53. data/spec/block_11_rtree_order_spec.rb +53 -0
  54. data/spec/block_12_batch_packed_spec.rb +55 -0
  55. data/spec/block_13_error_hardening_spec.rb +65 -0
  56. data/spec/block_14_memory_gc_hardening_spec.rb +116 -0
  57. data/spec/block_1_skeleton_spec.rb +45 -0
  58. data/spec/block_20_concurrency_spec.rb +157 -0
  59. data/spec/block_20_fuzz_spec.rb +145 -0
  60. data/spec/block_2_vendor_spec.rb +79 -0
  61. data/spec/block_3_geom_parse_spec.rb +89 -0
  62. data/spec/block_4_geom_api_spec.rb +90 -0
  63. data/spec/block_5_rect_api_spec.rb +96 -0
  64. data/spec/block_6_index_build_spec.rb +111 -0
  65. data/spec/block_7_index_owned_geometry_spec.rb +143 -0
  66. data/spec/block_8_index_borrowed_geometry_spec.rb +106 -0
  67. data/spec/block_9_flat_query_spec.rb +65 -0
  68. data/spec/expansion_a_auto_strategy_spec.rb +14 -0
  69. data/spec/expansion_b_registry_spec.rb +47 -0
  70. data/spec/expansion_c_active_record_source_spec.rb +42 -0
  71. data/spec/expansion_d_format_coverage_spec.rb +30 -0
  72. data/spec/expansion_e_low_level_geometry_spec.rb +82 -0
  73. data/spec/expansion_i_ractor_spec.rb +25 -0
  74. data/spec/expansion_j_full_tg_api_coverage_spec.rb +114 -0
  75. data/spec/spec_helper.rb +15 -0
  76. metadata +157 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 01f50a6c3b237bc983741eaf46a6440220c023dde0d91e8a1e64debb0dd8cf9e
4
+ data.tar.gz: e07214e6f201842029f415e1dc4c39f77cd9c003187cc9068013f72300f5398a
5
+ SHA512:
6
+ metadata.gz: c80efd671c29339c6023a4b7b616eb0f4eba73513aecea5dea1f239797a8c8c3c4b9a9fd12a1bc69d359a599b3dfc3c4891a8a2073d29e5525083538379b3dea
7
+ data.tar.gz: '0508a2a6f736791048212c214b75533131c83d0e38029a9b2a13d7860991f882868faeb6237f6305645fc0b4a4819ac0fd21b2c3968006e2a85d2b736425a7ff'
data/CHANGELOG.md ADDED
@@ -0,0 +1,103 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 - unreleased
4
+
5
+ Initial release-core implementation for `tg_geometry`.
6
+
7
+ ### Added
8
+
9
+ - Canonical public require path: `require "tg/geometry"`.
10
+ - Public namespace: `TG::Geometry`.
11
+ - Native extension build through `ext/tg_geometry/extconf.rb`.
12
+ - Vendored `tidwall/tg` and `tidwall/rtree.c` sources with pinned `VERSION` files and upstream license files.
13
+ - Error classes:
14
+ - `TG::Geometry::Error`
15
+ - `TG::Geometry::ParseError`
16
+ - `TG::Geometry::ArgumentError < ::ArgumentError`
17
+ - `TG::Geometry::FrozenIndexError`
18
+ - Immutable `TG::Geometry::Geom` parsing for GeoJSON, WKT, WKB, Hex, GeoBIN, and auto format detection.
19
+ - `TG::Geometry::Geom` methods:
20
+ - `#type`
21
+ - `#bbox`
22
+ - `#covers_xy?`
23
+ - `#contains?`
24
+ - `#intersects?`
25
+ - `#to_geojson`
26
+ - `#to_wkt`
27
+ - `#to_wkb`
28
+ - Immutable `TG::Geometry::Rect` API.
29
+ - Immutable `TG::Geometry::Index.build` with strict `[[id, object], ...]` entry format.
30
+ - Index ingestion modes:
31
+ - `via: :geom` borrowed geometry with `geom_owner` lifetime protection;
32
+ - `via: :geojson` owned geometry;
33
+ - `via: :wkb` owned geometry.
34
+ - Index strategies:
35
+ - `:flat`
36
+ - `:rtree`
37
+ - Deterministic insertion-order results for flat and rtree queries.
38
+ - Exact rtree memory accounting through a custom malloc/free allocator with headers.
39
+ - Native-endian packed point batch API: `TG::Geometry::Index#covering_ids_batch_packed`.
40
+ - Debug-only test hooks under `TG_DEBUG_TEST=1` for allocation failure simulation and byte counter inspection.
41
+ - Block 14 memory/GC/compaction hardening specs.
42
+ - Benchmark harnesses:
43
+ - `benchmark/parse_throughput.rb`
44
+ - `benchmark/gvl_threshold.rb`
45
+ - `benchmark/flat_vs_rtree.rb`
46
+ - `benchmark/batch_packed_vs_loop.rb`
47
+ - `benchmark/falcon_concurrency.rb`
48
+ - `benchmark/objectspace_memsize.rb`
49
+ - `benchmark/rss_stability.rb`
50
+ - Documentation:
51
+ - `docs/ARCHITECTURE.md`
52
+ - `docs/MEMORY_OWNERSHIP.md`
53
+ - `docs/CONCURRENCY.md`
54
+ - `docs/ERROR_HANDLING.md`
55
+ - `docs/BENCHMARKING.md`
56
+ - `docs/LIMITATIONS.md`
57
+ - `docs/RELEASE_CHECKLIST.md`
58
+
59
+ ### Not included
60
+
61
+ - `strategy: :auto` is not part of the release-core contract; it is tracked as Expansion Block A below.
62
+ - No Ractor support claim.
63
+ - No no-GVL execution.
64
+ - No full GIS, routing, geocoding, projections, geodesic helpers, nearest POI index, or result-geometry overlay operations.
65
+ - No public performance claims until benchmark results are produced by this gem.
66
+
67
+ ### OPEN QUESTION
68
+
69
+ - Final ASAN setup requires Roman approval before replacing the placeholder CI job.
70
+ - Final Valgrind setup requires Roman approval before replacing the placeholder CI job.
71
+
72
+ ## Unreleased
73
+
74
+ ### Added
75
+
76
+ - Expansion Block A status: `strategy: :auto` remains postponed for the first public release; explicit `:flat` / `:rtree` strategies are required.
77
+ - Expansion Block B: `TG::Geometry::Registry` Ruby helper for immutable Index reload/swap workflows.
78
+ - Expansion Block C: optional `TG::Geometry::ActiveRecordSource` helper that converts relation-like records into strict `[[id, object], ...]` entries without adding a Rails dependency.
79
+ - Expansion Block D: `TG::Geometry.parse_hex`, `TG::Geometry.parse_geobin`, `TG::Geometry::Geom#to_hex`, `#to_geobin`, and `#extra_json`.
80
+ - Expansion Block E: read-only borrowed low-level wrappers: `TG::Geometry::Line`, `TG::Geometry::Ring`, `TG::Geometry::Polygon`, plus `TG::Geometry::Geom#point`, `#line`, and `#polygon`.
81
+ - Expansion Block I: Ractor unsupported-boundary investigation documented in `docs/RACTOR.md` with specs asserting native wrappers are not treated as shareable Ractor objects.
82
+ - Expansion Block J grouped API coverage:
83
+ - safe point and empty geometry constructors;
84
+ - additional `TG::Geometry::Geom` predicates;
85
+ - geometry metadata and Z/M read accessors;
86
+ - MultiPoint/MultiLineString/MultiPolygon and GeometryCollection accessors;
87
+ - borrowed child `TG::Geometry::Geom` wrappers with `geom_owner`;
88
+ - value `TG::Geometry::Segment` wrappers from Line/Ring segment accessors.
89
+
90
+ ### Fixed
91
+
92
+ - Corrected `benchmark/gvl_threshold.rb` so each target size uses a valid WKT payload near that size instead of repeatedly benchmarking the same tiny polygon.
93
+
94
+ ### Documentation
95
+
96
+ - Added docs for Registry, ActiveRecord source helper, additional format coverage, low-level borrowed geometry wrappers, Ractor unsupported-boundary status, grouped full TG API coverage, Auto Strategy postponed status, and Expansion Blocks E–H status.
97
+
98
+ ### OPEN QUESTION
99
+
100
+ - Expansion Block F callback/search APIs remain blocked until a callback safety contract, exception semantics, GVL rules, and callback overhead benchmarks are approved.
101
+ - Expansion Block G no-allocation point query optimization remains blocked until boundary/hole-boundary equivalence tests and benchmarks prove it preserves `:covers` / `:contains` semantics.
102
+ - Expansion Block H geodesic/projection helpers remain blocked until an explicit optional dependency/API decision is approved.
103
+ - Remaining Expansion Block J scope such as Line/Ring/Polygon constructors, callback/search APIs, nearest segment APIs, global environment configuration, and allocator override APIs remains blocked until separate ownership/thread-safety contracts are approved.
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Roman Haydarov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,385 @@
1
+ # tg_geometry
2
+
3
+ `tg_geometry` is a Ruby C extension around the vendored `tidwall/tg` geometry
4
+ library and `tidwall/rtree.c`.
5
+
6
+ It exposes the public Ruby namespace `TG::Geometry` and the canonical require
7
+ path:
8
+
9
+ ```ruby
10
+ require "tg/geometry"
11
+ ```
12
+
13
+ The gem targets fast in-process planar geometry parsing, predicates,
14
+ format conversion, and geofencing-oriented immutable indexes. It does not try
15
+ to be a full GIS system.
16
+
17
+ ## Status
18
+
19
+ This repository is prepared as a first public release candidate with an
20
+ expanded API surface:
21
+
22
+ - release-core `Geom`, `Rect`, and immutable `Index` APIs;
23
+ - expanded format coverage for Hex and GeoBIN;
24
+ - read-only borrowed wrappers for lower-level TG geometry components;
25
+ - `Registry` reload/swap sugar;
26
+ - optional ActiveRecord-style source helpers that do not add a Rails runtime
27
+ dependency.
28
+
29
+ `strategy: :auto`, Ractor support, callback/search APIs, no-allocation point
30
+ query optimization, geodesic helpers, projections, and no-GVL execution are not
31
+ claimed in this release.
32
+
33
+ ## Installation
34
+
35
+ Add this line to your application's Gemfile:
36
+
37
+ ```ruby
38
+ gem "tg_geometry"
39
+ ```
40
+
41
+ Then run:
42
+
43
+ ```bash
44
+ bundle install
45
+ ```
46
+
47
+ The extension is built from vendored C sources. There is no GEOS, PostGIS,
48
+ PROJ, GDAL, system TG, or system rtree dependency.
49
+
50
+ Supported first-release platforms are Linux and macOS on x86_64/aarch64.
51
+ Windows is not supported in this release.
52
+
53
+ ## Basic parsing and predicates
54
+
55
+ ```ruby
56
+ require "tg/geometry"
57
+
58
+ zone = TG::Geometry.parse_geojson(<<~JSON)
59
+ {
60
+ "type": "Polygon",
61
+ "coordinates": [[[0,0], [10,0], [10,10], [0,10], [0,0]]]
62
+ }
63
+ JSON
64
+
65
+ zone.frozen? # => true
66
+ zone.type # => :polygon
67
+ zone.covers_xy?(5, 5) # => true
68
+ zone.covers_xy?(0, 0) # => true, boundary is covered
69
+ zone.bbox # => #<TG::Geometry::Rect ...>
70
+
71
+ wkt = zone.to_wkt
72
+ wkb = zone.to_wkb
73
+ ```
74
+
75
+ `TG::Geometry::Geom` objects are immutable. They cannot be manually allocated or
76
+ manually freed from Ruby. Native memory is released by Ruby GC through the typed
77
+ data wrapper.
78
+
79
+ ## Parse API
80
+
81
+ ```ruby
82
+ TG::Geometry.parse(str, format: :auto, index: :ystripes)
83
+ TG::Geometry.parse_geojson(str, index: :ystripes)
84
+ TG::Geometry.parse_wkt(str, index: :ystripes)
85
+ TG::Geometry.parse_wkb(bytes, index: :ystripes)
86
+ TG::Geometry.parse_hex(str, index: :ystripes)
87
+ TG::Geometry.parse_geobin(bytes, index: :ystripes)
88
+ ```
89
+
90
+ Accepted `format:` values for `parse` are:
91
+
92
+ - `:auto`
93
+ - `:geojson`
94
+ - `:wkt`
95
+ - `:wkb`
96
+ - `:hex`
97
+ - `:geobin`
98
+
99
+ Accepted TG internal polygon index values are:
100
+
101
+ - `:default`
102
+ - `:none`
103
+ - `:natural`
104
+ - `:ystripes`
105
+
106
+ Parse failures raise `TG::Geometry::ParseError`. Invalid options raise
107
+ `TG::Geometry::ArgumentError`, which inherits from Ruby's `::ArgumentError`.
108
+
109
+ ## Geom API
110
+
111
+ Release-core methods:
112
+
113
+ ```ruby
114
+ geom.type
115
+ geom.bbox
116
+ geom.covers_xy?(x, y)
117
+ geom.contains?(other_geom)
118
+ geom.intersects?(other_geom)
119
+ geom.to_geojson
120
+ geom.to_wkt
121
+ geom.to_wkb
122
+ ```
123
+
124
+ Expanded methods include additional predicates, format writers, metadata
125
+ accessors, and read-only borrowed child wrappers. See:
126
+
127
+ - `docs/FORMAT_COVERAGE.md`
128
+ - `docs/LOW_LEVEL_GEOMETRY.md`
129
+ - `docs/FULL_TG_API_COVERAGE.md`
130
+
131
+ For point predicates, this release prioritizes exact `covers` / `contains`
132
+ semantics over the fastest possible no-allocation path. Query methods construct
133
+ a temporary TG point geometry and free it before returning. A future optimized
134
+ point path requires boundary and hole-boundary equivalence tests plus benchmark
135
+ proof.
136
+
137
+ ## Rect API
138
+
139
+ ```ruby
140
+ rect = TG::Geometry::Rect.new(0, 0, 10, 10)
141
+
142
+ rect.min_x
143
+ rect.min_y
144
+ rect.max_x
145
+ rect.max_y
146
+ rect.center # => [5.0, 5.0]
147
+ rect.contains_point?(5, 5) # => true
148
+ rect.intersects?(other_rect)
149
+ rect.expand_to_include(other_rect)
150
+ rect.expand_to_include_point(x, y)
151
+ ```
152
+
153
+ `Rect` rejects non-finite coordinates and invalid coordinate order. It is frozen
154
+ after construction.
155
+
156
+ There is intentionally no first-release `Rect#contains?` method because the name
157
+ is ambiguous. Use `contains_point?`.
158
+
159
+ ## Immutable Index
160
+
161
+ `TG::Geometry::Index` is built once and then read-only forever.
162
+
163
+ ```ruby
164
+ entries = [
165
+ [:zone_a, '{"type":"Polygon","coordinates":[[[0,0],[10,0],[10,10],[0,10],[0,0]]]}'],
166
+ [:zone_b, '{"type":"Polygon","coordinates":[[[20,20],[30,20],[30,30],[20,30],[20,20]]]}']
167
+ ]
168
+
169
+ index = TG::Geometry::Index.build(
170
+ entries,
171
+ via: :geojson,
172
+ strategy: :rtree,
173
+ predicate: :covers,
174
+ geometry_index: :ystripes
175
+ )
176
+
177
+ index.frozen? # => true
178
+ index.size # => 2
179
+ index.strategy # => :rtree
180
+ index.predicate # => :covers
181
+ index.find_covering(5, 5) # => :zone_a
182
+ index.covering_ids(5, 5) # => [:zone_a]
183
+ index.intersecting_rect(0, 0, 25, 25)
184
+ ```
185
+
186
+ Accepted input format:
187
+
188
+ ```ruby
189
+ [[id1, object1], [id2, object2], ...]
190
+ ```
191
+
192
+ Rules:
193
+
194
+ - `entries` must be an Array.
195
+ - Every entry must be a two-element Array.
196
+ - `id` may be any Ruby object except `nil`.
197
+ - `false` ids are accepted, but discouraged because `find_covering` uses `nil`
198
+ for no match.
199
+ - Duplicate ids are allowed.
200
+ - Returned ids are the same Ruby objects stored in the index; they are not
201
+ copied, frozen, stringified, or duplicated.
202
+ - Result order is insertion order for both `:flat` and `:rtree`.
203
+
204
+ Accepted `via:` modes:
205
+
206
+ - `:geom` — borrow an existing `TG::Geometry::Geom`; the index marks the owner
207
+ wrapper so the borrowed native pointer remains valid.
208
+ - `:geojson` — parse and own native TG geometries inside the index.
209
+ - `:wkb` — parse and own native TG geometries inside the index.
210
+
211
+ Accepted strategies:
212
+
213
+ - `:flat`
214
+ - `:rtree`
215
+
216
+ `strategy: :auto` is not exposed in this release. The benchmark output does not
217
+ support a single universal threshold: flat scan may win for early insertion-order
218
+ hits or heavily overlapping datasets, while rtree may win for misses, later hits,
219
+ or selective rectangle queries. Choose the strategy explicitly and benchmark on
220
+ your own data.
221
+
222
+ Accepted predicates:
223
+
224
+ - `:covers` — default for geofencing; boundary points are included.
225
+ - `:contains` — stricter OGC-style containment semantics.
226
+
227
+ ## Packed batch point queries
228
+
229
+ For high-throughput same-process point lookups, the index supports a packed
230
+ native-endian double input format:
231
+
232
+ ```ruby
233
+ points = [5.0, 5.0, 25.0, 25.0].pack("d*")
234
+ index.covering_ids_batch_packed(points)
235
+ # => [:zone_a, :zone_b]
236
+ ```
237
+
238
+ Input format:
239
+
240
+ - Ruby String treated as raw bytes.
241
+ - Native-endian doubles.
242
+ - Pairs of `lon, lat`.
243
+ - Length must be a multiple of 16 bytes.
244
+ - Empty string returns `[]`.
245
+
246
+ This format is intentionally native-endian for same-process speed and simplicity.
247
+ Do not use it as a cross-platform serialized file format.
248
+
249
+ ## Registry reload pattern
250
+
251
+ `Registry` is Ruby-level sugar over immutable indexes:
252
+
253
+ ```ruby
254
+ class DeliveryZones < TG::Geometry::Registry
255
+ source do
256
+ [
257
+ [:zone_a, '{"type":"Polygon","coordinates":[[[0,0],[10,0],[10,10],[0,10],[0,0]]]}']
258
+ ]
259
+ end
260
+
261
+ index_options via: :geojson, strategy: :rtree, predicate: :covers
262
+ end
263
+
264
+ registry = DeliveryZones.new
265
+ registry.reload!
266
+ registry.find_covering(5, 5)
267
+ ```
268
+
269
+ Reload builds a new full immutable index first and swaps the reference only after
270
+ successful build:
271
+
272
+ ```ruby
273
+ new_index = TG::Geometry::Index.build(entries, via: :geojson, strategy: :rtree)
274
+ @index = new_index
275
+ ```
276
+
277
+ Old indexes remain alive while existing readers hold references to them. There
278
+ is no in-place mutation, no public `add`, `delete`, `clear`, or `rebuild!` API on
279
+ `Index`.
280
+
281
+ See `docs/REGISTRY.md` and `docs/ACTIVE_RECORD.md` for the expanded helpers.
282
+
283
+ ## Memory ownership model
284
+
285
+ The implementation uses explicit allocator pairs and GC accounting:
286
+
287
+ | Resource | Allocator | Deallocator | Owner |
288
+ |---|---|---|---|
289
+ | `tg_geom_wrapper_t` | `TypedData_Make_Struct` / Ruby allocator | `ruby_xfree` | Ruby `Geom` object |
290
+ | TG geometry in `Geom` | TG parser/constructor | `tg_geom_free` | `Geom` wrapper |
291
+ | `tg_index_t` | `TypedData_Make_Struct` / Ruby allocator | `ruby_xfree` | Ruby `Index` object |
292
+ | Index entries array | `calloc` | `free` | `Index` |
293
+ | TG geometry via `:geojson` / `:wkb` | TG parser | `tg_geom_free` | `Index` |
294
+ | TG geometry via `:geom` | Existing `Geom` wrapper | Existing `Geom` wrapper | Borrowed by `Index` through `geom_owner` |
295
+ | rtree internals | custom `tg_rtree_malloc` with header | custom `tg_rtree_free` | rtree / `Index` accounting |
296
+ | Ruby ids | Ruby VM | Ruby GC | Marked and compacted by `Index` |
297
+
298
+ `ObjectSpace.memsize_of(index)` includes entries, owned TG geometries, and exact
299
+ rtree allocation bytes. Borrowed geometries are not double-counted by the index.
300
+
301
+ See `docs/MEMORY_OWNERSHIP.md` for the full table and cleanup rules.
302
+
303
+ ## Concurrency model
304
+
305
+ `Index` and `Geom` are immutable after construction. Concurrent read-only use
306
+ from normal Ruby threads is supported by design and covered by tests.
307
+
308
+ The first release keeps GVL for parse, write, query, batch, and rtree build/free
309
+ paths. This is intentional: the rtree allocator calls Ruby GC accounting APIs,
310
+ and no-GVL execution would require separate input-copying and allocator-accounting
311
+ design.
312
+
313
+ No Ractor support is claimed.
314
+
315
+ See `docs/CONCURRENCY.md` and `docs/RACTOR.md`.
316
+
317
+ ## Benchmarks
318
+
319
+ Benchmark scripts live in `benchmark/`:
320
+
321
+ ```bash
322
+ bundle exec ruby benchmark/parse_throughput.rb
323
+ bundle exec ruby benchmark/flat_vs_rtree.rb
324
+ bundle exec ruby benchmark/batch_packed_vs_loop.rb
325
+ bundle exec ruby benchmark/objectspace_memsize.rb
326
+ bundle exec ruby benchmark/rss_stability.rb
327
+ bundle exec ruby benchmark/gvl_threshold.rb
328
+ bundle exec ruby benchmark/falcon_concurrency.rb
329
+ ```
330
+
331
+ By default, benchmarks use a fast local matrix. Set `TGEOMETRY_BENCH_FULL=1` for
332
+ the larger matrix where supported.
333
+
334
+ The repository benchmarks are engineering tools, not universal marketing claims.
335
+ Do not copy upstream TG C benchmark numbers as Ruby gem performance claims.
336
+
337
+ ## Limitations
338
+
339
+ `tg_geometry` is not a full GIS system.
340
+
341
+ Not included in this release:
342
+
343
+ - geocoding;
344
+ - routing;
345
+ - projections;
346
+ - geodesic distance/area;
347
+ - buffer / union / difference / overlay result geometry operations;
348
+ - nearest POI index;
349
+ - Rails dependency in the core extension;
350
+ - Redis or external service dependency;
351
+ - public callback/search APIs;
352
+ - Ractor support claim;
353
+ - no-GVL execution claim;
354
+ - universal `:auto` strategy.
355
+
356
+ TG works in planar XY coordinates. If lon/lat coordinates are passed in, length,
357
+ area, and perimeter-style values are in input coordinate units, not meters.
358
+ Use PostGIS, GEOS, PROJ, or other GIS tooling when full GIS functionality is
359
+ needed.
360
+
361
+ ## Development
362
+
363
+ ```bash
364
+ bundle install
365
+ bundle exec rake compile
366
+ bundle exec rake spec
367
+ ```
368
+
369
+ Useful targeted checks:
370
+
371
+ ```bash
372
+ bundle exec rspec spec/block_12_batch_packed_spec.rb
373
+ bundle exec rspec spec/block_14_memory_gc_hardening_spec.rb
374
+ bundle exec rspec spec/block_20_concurrency_spec.rb
375
+ bundle exec rspec spec/block_20_fuzz_spec.rb
376
+ ```
377
+
378
+ Memory-tool CI jobs for ASAN and Valgrind are intentionally left as OPEN QUESTION
379
+ placeholders until the exact setup is approved. Do not replace them with guessed
380
+ configuration.
381
+
382
+ ## License
383
+
384
+ MIT. Vendored upstream license files for `tidwall/tg` and `tidwall/rtree.c` are
385
+ included under `ext/tg_geometry/vendor/`.
data/Rakefile ADDED
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "fileutils"
5
+ require "rbconfig"
6
+ require "rake/clean"
7
+
8
+ EXT_NAME = "tg_geometry_ext_geometry_ext"
9
+ EXT_DIR = File.expand_path("ext/tg_geometry", __dir__)
10
+ LIB_DIR = File.expand_path("lib", __dir__)
11
+ DLEXT = RbConfig::CONFIG.fetch("DLEXT")
12
+ PLATFORM = RbConfig::CONFIG.fetch("arch")
13
+ BUILD_RUBY_VERSION = RUBY_VERSION
14
+ BUILD_DIR = File.expand_path(File.join("tmp", PLATFORM, EXT_NAME, BUILD_RUBY_VERSION), __dir__)
15
+ EXT_SO = File.join(EXT_DIR, "#{EXT_NAME}.#{DLEXT}")
16
+ LIB_SO = File.join(LIB_DIR, "#{EXT_NAME}.#{DLEXT}")
17
+
18
+ EXT_BUILD_GLOBS = [
19
+ File.join(EXT_DIR, "Makefile"),
20
+ File.join(EXT_DIR, "mkmf.log"),
21
+ File.join(EXT_DIR, "*.o"),
22
+ File.join(EXT_DIR, "*.so"),
23
+ File.join(EXT_DIR, "*.bundle"),
24
+ File.join(EXT_DIR, "*.dll"),
25
+ File.join(EXT_DIR, "*.dylib")
26
+ ].freeze
27
+
28
+ CLEAN.include(
29
+ "tmp",
30
+ "lib/*.so",
31
+ "lib/*.bundle",
32
+ "lib/*.dll",
33
+ "lib/*.dylib",
34
+ *EXT_BUILD_GLOBS
35
+ )
36
+
37
+ # Keep the source tree clean like pq_crypto: ext/tg_geometry contains the checked-in
38
+ # extension sources and ext/tg_geometry/vendor contains the checked-in vendored C
39
+ # sources, while Makefile/mkmf.log/object/shared-library build outputs live in tmp/.
40
+ def remove_in_place_extension_artifacts!
41
+ EXT_BUILD_GLOBS.each { |pattern| FileUtils.rm_f(Dir[pattern]) }
42
+ end
43
+
44
+ def build_extension!(debug: false)
45
+ remove_in_place_extension_artifacts!
46
+ FileUtils.rm_rf(BUILD_DIR)
47
+ FileUtils.mkdir_p(BUILD_DIR)
48
+
49
+ env = debug ? { "TG_DEBUG_TEST" => "1" } : {}
50
+ Dir.chdir(BUILD_DIR) do
51
+ sh(env, RbConfig.ruby, File.join(EXT_DIR, "extconf.rb"))
52
+ sh "make"
53
+ end
54
+
55
+ FileUtils.mkdir_p(LIB_DIR)
56
+ FileUtils.cp(File.join(BUILD_DIR, "#{EXT_NAME}.#{DLEXT}"), LIB_SO)
57
+ end
58
+
59
+ desc "Compile the native extension into tmp/ and copy it to lib/"
60
+ task "compile" do
61
+ build_extension!(debug: false)
62
+ end
63
+
64
+ desc "Compile the native extension with test-only debug hooks"
65
+ task "compile:test" do
66
+ build_extension!(debug: true)
67
+ end
68
+
69
+ begin
70
+ require "rspec/core/rake_task"
71
+
72
+ RSpec::Core::RakeTask.new(:spec => "compile:test")
73
+
74
+ task :gc_stress_env do
75
+ ENV["RUBY_GC_STRESS"] = "1"
76
+ end
77
+
78
+ namespace :spec do
79
+ RSpec::Core::RakeTask.new(:gc_stress => ["compile:test", "gc_stress_env"]) do |task|
80
+ task.rspec_opts = ["--tag", "~skip"]
81
+ task.verbose = true
82
+ end
83
+
84
+ RSpec::Core::RakeTask.new(:gc_compact => "compile:test") do |task|
85
+ task.pattern = "spec/block_3_geom_parse_spec.rb spec/block_6_index_build_spec.rb spec/block_8_index_borrowed_geometry_spec.rb spec/block_14_memory_gc_hardening_spec.rb"
86
+ end
87
+ end
88
+ rescue LoadError
89
+ task :spec do
90
+ abort "RSpec is required to run the test suite. Install development dependencies with bundle install."
91
+ end
92
+
93
+ namespace :spec do
94
+ task :gc_stress => :spec
95
+ task :gc_compact => :spec
96
+ end
97
+ end
98
+
99
+ namespace :benchmark do
100
+ Dir[File.join(__dir__, "benchmark", "*.rb")].sort.each do |path|
101
+ next if File.basename(path).start_with?("_")
102
+
103
+ name = File.basename(path, ".rb")
104
+ desc "Run benchmark/#{name}.rb"
105
+ task name => :compile do
106
+ ruby path
107
+ end
108
+ end
109
+ end
110
+
111
+ namespace :vendor do
112
+ desc "Sync vendored tidwall/tg and rtree.c sources to pinned commits"
113
+ task :sync do
114
+ ruby "script/vendor_libs.rb", "--sync"
115
+ end
116
+
117
+ desc "Verify vendored tidwall/tg and rtree.c sources against pinned tree SHA256"
118
+ task :verify do
119
+ ruby "script/vendor_libs.rb", "--verify"
120
+ end
121
+ end
122
+
123
+ desc "Alias for vendor:sync"
124
+ task vendor: "vendor:sync"
125
+
126
+ desc "Vendor sources, compile, run specs"
127
+ task full_build: ["vendor:verify", "compile", "spec"]
128
+
129
+ task default: :spec