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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +103 -0
- data/Gemfile +3 -0
- data/LICENSE.txt +21 -0
- data/README.md +385 -0
- data/Rakefile +129 -0
- data/benchmark/_support.rb +115 -0
- data/benchmark/batch_packed_vs_loop.rb +27 -0
- data/benchmark/falcon_concurrency.rb +25 -0
- data/benchmark/flat_vs_rtree.rb +27 -0
- data/benchmark/gvl_threshold.rb +41 -0
- data/benchmark/objectspace_memsize.rb +17 -0
- data/benchmark/parse_throughput.rb +38 -0
- data/benchmark/rss_stability.rb +70 -0
- data/docs/ACTIVE_RECORD.md +26 -0
- data/docs/ARCHITECTURE.md +130 -0
- data/docs/AUTO_STRATEGY.md +15 -0
- data/docs/BENCHMARKING.md +75 -0
- data/docs/CASUAL_EXAMPLE.md +618 -0
- data/docs/CONCURRENCY.md +65 -0
- data/docs/ERROR_HANDLING.md +55 -0
- data/docs/EXPANSION_E_TO_H_STATUS.md +51 -0
- data/docs/FORMAT_COVERAGE.md +23 -0
- data/docs/FULL_TG_API_COVERAGE.md +109 -0
- data/docs/LIMITATIONS.md +61 -0
- data/docs/LOW_LEVEL_GEOMETRY.md +121 -0
- data/docs/MEMORY_OWNERSHIP.md +94 -0
- data/docs/RACTOR.md +40 -0
- data/docs/REGISTRY.md +37 -0
- data/docs/RELEASE_CHECKLIST.md +39 -0
- data/ext/tg_geometry/extconf.rb +91 -0
- data/ext/tg_geometry/tg_geometry_ext.c +3054 -0
- data/ext/tg_geometry/tg_geometry_vendor_rtree.c +1 -0
- data/ext/tg_geometry/tg_geometry_vendor_tg.c +24 -0
- data/ext/tg_geometry/vendor/.vendored +16 -0
- data/ext/tg_geometry/vendor/rtree/LICENSE +20 -0
- data/ext/tg_geometry/vendor/rtree/README.md +202 -0
- data/ext/tg_geometry/vendor/rtree/VERSION +3 -0
- data/ext/tg_geometry/vendor/rtree/rtree.c +840 -0
- data/ext/tg_geometry/vendor/rtree/rtree.h +105 -0
- data/ext/tg_geometry/vendor/tg/LICENSE +19 -0
- data/ext/tg_geometry/vendor/tg/README.md +197 -0
- data/ext/tg_geometry/vendor/tg/VERSION +3 -0
- data/ext/tg_geometry/vendor/tg/tg.c +16010 -0
- data/ext/tg_geometry/vendor/tg/tg.h +359 -0
- data/lib/tg/geometry/active_record_source.rb +57 -0
- data/lib/tg/geometry/registry.rb +119 -0
- data/lib/tg/geometry/version.rb +7 -0
- data/lib/tg/geometry.rb +6 -0
- data/lib/tg_geometry.rb +3 -0
- data/script/vendor_libs.rb +264 -0
- data/spec/block_10_rtree_strategy_spec.rb +82 -0
- data/spec/block_11_rtree_order_spec.rb +53 -0
- data/spec/block_12_batch_packed_spec.rb +55 -0
- data/spec/block_13_error_hardening_spec.rb +65 -0
- data/spec/block_14_memory_gc_hardening_spec.rb +116 -0
- data/spec/block_1_skeleton_spec.rb +45 -0
- data/spec/block_20_concurrency_spec.rb +157 -0
- data/spec/block_20_fuzz_spec.rb +145 -0
- data/spec/block_2_vendor_spec.rb +79 -0
- data/spec/block_3_geom_parse_spec.rb +89 -0
- data/spec/block_4_geom_api_spec.rb +90 -0
- data/spec/block_5_rect_api_spec.rb +96 -0
- data/spec/block_6_index_build_spec.rb +111 -0
- data/spec/block_7_index_owned_geometry_spec.rb +143 -0
- data/spec/block_8_index_borrowed_geometry_spec.rb +106 -0
- data/spec/block_9_flat_query_spec.rb +65 -0
- data/spec/expansion_a_auto_strategy_spec.rb +14 -0
- data/spec/expansion_b_registry_spec.rb +47 -0
- data/spec/expansion_c_active_record_source_spec.rb +42 -0
- data/spec/expansion_d_format_coverage_spec.rb +30 -0
- data/spec/expansion_e_low_level_geometry_spec.rb +82 -0
- data/spec/expansion_i_ractor_spec.rb +25 -0
- data/spec/expansion_j_full_tg_api_coverage_spec.rb +114 -0
- data/spec/spec_helper.rb +15 -0
- metadata +157 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Error handling
|
|
2
|
+
|
|
3
|
+
## Exception hierarchy
|
|
4
|
+
|
|
5
|
+
```ruby
|
|
6
|
+
module TG
|
|
7
|
+
module Geometry
|
|
8
|
+
class Error < StandardError; end
|
|
9
|
+
class ParseError < Error; end
|
|
10
|
+
class ArgumentError < ::ArgumentError; end
|
|
11
|
+
class FrozenIndexError < Error; end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
`TG::Geometry::ArgumentError` inherits from Ruby `::ArgumentError` so invalid user arguments remain rescuable through the standard Ruby error class.
|
|
17
|
+
|
|
18
|
+
## Mapping
|
|
19
|
+
|
|
20
|
+
Condition | Ruby exception | Cleanup rule
|
|
21
|
+
--- | --- | ---
|
|
22
|
+
TG parse error | `TG::Geometry::ParseError` | copy error string, free TG error geometry, then raise
|
|
23
|
+
invalid symbol option | `TG::Geometry::ArgumentError` | no native state if validated before allocation; otherwise dispose partial state
|
|
24
|
+
nil id | `TG::Geometry::ArgumentError` | partial Index disposed immediately
|
|
25
|
+
wrong object class/type | `TypeError` | use Ruby type checks / `TypedData_Get_Struct`
|
|
26
|
+
non-finite coordinate | `TG::Geometry::ArgumentError` | reject before point or rect work where possible
|
|
27
|
+
entries allocation failure | `NoMemoryError` | no entries state installed, or partial wrapper left empty
|
|
28
|
+
rtree allocation failure | `NoMemoryError` | dispose rtree/entries/owned geometries immediately
|
|
29
|
+
match buffer allocation failure | `NoMemoryError` | free any candidate buffer / query point before raising
|
|
30
|
+
internal writer size mismatch | `TG::Geometry::Error` | no native ownership change
|
|
31
|
+
|
|
32
|
+
## Parse errors
|
|
33
|
+
|
|
34
|
+
TG parser failures return a geometry object that carries an error string. That object owns resources and must be freed.
|
|
35
|
+
|
|
36
|
+
The implementation copies the error string before `tg_geom_free`, then raises `TG::Geometry::ParseError` from the copied Ruby string. It never reads `tg_geom_error` after freeing the error geometry.
|
|
37
|
+
|
|
38
|
+
## Build failure atomicity
|
|
39
|
+
|
|
40
|
+
`TG::Geometry::Index.build` is atomic: users either receive a frozen fully built Index or an exception. They never observe a partially built Index.
|
|
41
|
+
|
|
42
|
+
The build body is protected with `rb_protect`. If it raises, `index_dispose` runs immediately and frees:
|
|
43
|
+
|
|
44
|
+
1. rtree internals;
|
|
45
|
+
2. initialized owned geometries;
|
|
46
|
+
3. entries array;
|
|
47
|
+
4. byte counters.
|
|
48
|
+
|
|
49
|
+
## Rtree callbacks
|
|
50
|
+
|
|
51
|
+
Rtree allocator callbacks return `NULL` on OOM. Rtree search callbacks record failure in C state or avoid failure by preallocating local mark buffers before search. Ruby exceptions do not longjmp through `rtree.c` traversal.
|
|
52
|
+
|
|
53
|
+
## Writer safety
|
|
54
|
+
|
|
55
|
+
Text writers allocate Ruby strings with one extra byte for the null terminator, call the TG writer with that capacity, then set the Ruby string length back to the required content length. WKB allocates exactly the required binary length and associates `ASCII-8BIT` encoding.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Expansion Blocks E–H status
|
|
2
|
+
|
|
3
|
+
## Expansion Block E — Low-level Ring / Line / Polygon APIs
|
|
4
|
+
|
|
5
|
+
Implemented as read-only borrowed wrappers:
|
|
6
|
+
|
|
7
|
+
- `TG::Geometry::Geom#point`
|
|
8
|
+
- `TG::Geometry::Geom#line`
|
|
9
|
+
- `TG::Geometry::Geom#polygon`
|
|
10
|
+
- `TG::Geometry::Line`
|
|
11
|
+
- `TG::Geometry::Ring`
|
|
12
|
+
- `TG::Geometry::Polygon`
|
|
13
|
+
|
|
14
|
+
Invariant satisfied: child pointers are borrowed from the parent TG geometry and the wrapper keeps the parent Ruby `TG::Geometry::Geom` alive through `geom_owner` with compaction-aware marking.
|
|
15
|
+
|
|
16
|
+
Tests: `spec/expansion_e_low_level_geometry_spec.rb` covers accessors, private allocation, out-of-range errors, and parent survival after `GC.start` / `GC.compact`.
|
|
17
|
+
|
|
18
|
+
## Expansion Block F — Callback/search APIs
|
|
19
|
+
|
|
20
|
+
OPEN QUESTION: not implemented.
|
|
21
|
+
|
|
22
|
+
Reason: the roadmap allows callback/search APIs only with a new callback safety contract. The current contract still forbids public callback/block APIs in the first release. No `geom.search`, `index.search`, or `each_match` method is exposed.
|
|
23
|
+
|
|
24
|
+
Required before implementation:
|
|
25
|
+
|
|
26
|
+
- explicit callback exception propagation semantics;
|
|
27
|
+
- GVL behavior for Ruby callbacks inside C loops;
|
|
28
|
+
- proof that borrowed pointers cannot be invalidated by callback reentrancy;
|
|
29
|
+
- benchmark of callback overhead;
|
|
30
|
+
- tests for exceptions raised from callbacks.
|
|
31
|
+
|
|
32
|
+
## Expansion Block G — Fast no-allocation point query optimization
|
|
33
|
+
|
|
34
|
+
OPEN QUESTION: not implemented.
|
|
35
|
+
|
|
36
|
+
Reason: the current implementation intentionally constructs a temporary TG point geometry for `covers` / `contains` correctness. Replacing this with `tg_geom_intersects_xy` or a specialized helper requires proof of exact boundary semantics and a benchmark proving benefit.
|
|
37
|
+
|
|
38
|
+
Current invariant: point query semantics remain exact and boundary behavior is covered by existing tests.
|
|
39
|
+
|
|
40
|
+
## Expansion Block H — Geodesic helpers or projection integration
|
|
41
|
+
|
|
42
|
+
OPEN QUESTION: not implemented.
|
|
43
|
+
|
|
44
|
+
Reason: TG core remains planar XY. No optional geodesic/projection dependency has been approved. The gem continues to document that `length`, `area`, and `perimeter` are in input coordinate units, not meters for lon/lat.
|
|
45
|
+
|
|
46
|
+
Required before implementation:
|
|
47
|
+
|
|
48
|
+
- explicit optional dependency decision;
|
|
49
|
+
- public API shape;
|
|
50
|
+
- tests separating planar TG behavior from geodesic/projection helpers;
|
|
51
|
+
- documentation that TG itself does not handle geodesics.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Format coverage
|
|
2
|
+
|
|
3
|
+
Expansion Block D exposes additional TG format helpers without changing ownership rules.
|
|
4
|
+
|
|
5
|
+
## Parse helpers
|
|
6
|
+
|
|
7
|
+
- `TG::Geometry.parse_hex(str, index: :ystripes)`
|
|
8
|
+
- `TG::Geometry.parse_geobin(bytes, index: :ystripes)`
|
|
9
|
+
|
|
10
|
+
These are shortcuts over `TG::Geometry.parse(..., format: :hex)` and `TG::Geometry.parse(..., format: :geobin)`.
|
|
11
|
+
|
|
12
|
+
## Writer helpers
|
|
13
|
+
|
|
14
|
+
- `TG::Geometry::Geom#to_hex` -> UTF-8 String
|
|
15
|
+
- `TG::Geometry::Geom#to_geobin` -> ASCII-8BIT String
|
|
16
|
+
|
|
17
|
+
Writers use the same direct Ruby string buffer pattern as existing writers. Hex is text and GeoBIN is binary.
|
|
18
|
+
|
|
19
|
+
## Raw extra_json
|
|
20
|
+
|
|
21
|
+
- `TG::Geometry::Geom#extra_json` -> UTF-8 String or nil
|
|
22
|
+
|
|
23
|
+
This returns a copied Ruby String from TG's raw extra JSON pointer. It does not parse JSON into Hashes and does not expose borrowed child wrappers. This follows the constraint that raw `extra_json` is safer than implicit `JSON.parse`.
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# Full TG API coverage status
|
|
2
|
+
|
|
3
|
+
This document covers Expansion Block J as implemented in grouped, safe increments.
|
|
4
|
+
|
|
5
|
+
Block J does not mean exposing every upstream TG function in one change. The rule is: implement in groups, define ownership for each group, add tests, and avoid environment allocator overrides or global mutable settings.
|
|
6
|
+
|
|
7
|
+
## Implemented group: predicates
|
|
8
|
+
|
|
9
|
+
`TG::Geometry::Geom` now exposes additional read-only predicates:
|
|
10
|
+
|
|
11
|
+
- `equals?(other)`
|
|
12
|
+
- `disjoint?(other)`
|
|
13
|
+
- `within?(other)`
|
|
14
|
+
- `covers?(other)`
|
|
15
|
+
- `covered_by?(other)`
|
|
16
|
+
- `touches?(other)`
|
|
17
|
+
- `intersects_xy?(x, y)`
|
|
18
|
+
- `intersects_rect?(rect)`
|
|
19
|
+
- `intersects_rect?(min_x, min_y, max_x, max_y)`
|
|
20
|
+
|
|
21
|
+
Existing methods remain:
|
|
22
|
+
|
|
23
|
+
- `contains?(other)`
|
|
24
|
+
- `intersects?(other)`
|
|
25
|
+
- `covers_xy?(x, y)`
|
|
26
|
+
|
|
27
|
+
`equals?` intentionally does not change Ruby `==`, `eql?`, or `hash`. Ruby equality remains identity-based until that open API decision is explicitly made.
|
|
28
|
+
|
|
29
|
+
## Implemented group: geometry accessors
|
|
30
|
+
|
|
31
|
+
`TG::Geometry::Geom` now exposes safe read-only accessors for upstream geometry collections and coordinate metadata:
|
|
32
|
+
|
|
33
|
+
- `feature?`
|
|
34
|
+
- `feature_collection?`
|
|
35
|
+
- `empty?`
|
|
36
|
+
- `dims`
|
|
37
|
+
- `has_z?`
|
|
38
|
+
- `has_m?`
|
|
39
|
+
- `z`
|
|
40
|
+
- `m`
|
|
41
|
+
- `extra_coords`
|
|
42
|
+
- `num_points`
|
|
43
|
+
- `point_at(index)`
|
|
44
|
+
- `points`
|
|
45
|
+
- `num_lines`
|
|
46
|
+
- `line_at(index)`
|
|
47
|
+
- `lines`
|
|
48
|
+
- `num_polygons`
|
|
49
|
+
- `polygon_at(index)`
|
|
50
|
+
- `polygons`
|
|
51
|
+
- `num_geometries`
|
|
52
|
+
- `geometry_at(index)`
|
|
53
|
+
- `geometries`
|
|
54
|
+
|
|
55
|
+
`geometry_at` and `geometries` return borrowed immutable `TG::Geometry::Geom` wrappers. They keep the parent wrapper alive through `geom_owner`, mark it for GC, update it during compaction, and do not call `tg_geom_free` for the borrowed child pointer.
|
|
56
|
+
|
|
57
|
+
## Implemented group: point and empty constructors
|
|
58
|
+
|
|
59
|
+
Safe constructors are exposed only where ownership is simple and the returned object owns exactly one `struct tg_geom *`:
|
|
60
|
+
|
|
61
|
+
- `TG::Geometry.point(x, y)`
|
|
62
|
+
- `TG::Geometry.point_z(x, y, z)`
|
|
63
|
+
- `TG::Geometry.point_m(x, y, m)`
|
|
64
|
+
- `TG::Geometry.point_zm(x, y, z, m)`
|
|
65
|
+
- `TG::Geometry.empty_point`
|
|
66
|
+
- `TG::Geometry.empty_linestring`
|
|
67
|
+
- `TG::Geometry.empty_polygon`
|
|
68
|
+
- `TG::Geometry.empty_multipoint`
|
|
69
|
+
- `TG::Geometry.empty_multilinestring`
|
|
70
|
+
- `TG::Geometry.empty_multipolygon`
|
|
71
|
+
- `TG::Geometry.empty_geometrycollection`
|
|
72
|
+
|
|
73
|
+
Numeric coordinates are converted with `NUM2DBL` and rejected if NaN or Infinity.
|
|
74
|
+
|
|
75
|
+
## Implemented group: segments
|
|
76
|
+
|
|
77
|
+
`TG::Geometry::Segment` is a frozen value wrapper over one copied `struct tg_segment`.
|
|
78
|
+
|
|
79
|
+
Created by:
|
|
80
|
+
|
|
81
|
+
- `TG::Geometry::Line#segment_at(index)`
|
|
82
|
+
- `TG::Geometry::Line#segments`
|
|
83
|
+
- `TG::Geometry::Ring#segment_at(index)`
|
|
84
|
+
- `TG::Geometry::Ring#segments`
|
|
85
|
+
|
|
86
|
+
Methods:
|
|
87
|
+
|
|
88
|
+
- `a -> [Float, Float]`
|
|
89
|
+
- `b -> [Float, Float]`
|
|
90
|
+
- `points -> [[Float, Float], [Float, Float]]`
|
|
91
|
+
- `bbox -> TG::Geometry::Rect`
|
|
92
|
+
- `intersects?(other_segment)`
|
|
93
|
+
|
|
94
|
+
`TG::Geometry::Segment.allocate` is disabled. Segment wrappers do not borrow parent TG memory and do not call any TG free function.
|
|
95
|
+
|
|
96
|
+
## Still not implemented
|
|
97
|
+
|
|
98
|
+
The following remain outside this grouped implementation:
|
|
99
|
+
|
|
100
|
+
- Line/Ring/Polygon public constructors from Ruby arrays;
|
|
101
|
+
- MultiLineString / MultiPolygon constructors from low-level wrappers;
|
|
102
|
+
- callback/search APIs;
|
|
103
|
+
- nearest segment APIs;
|
|
104
|
+
- environment configuration functions;
|
|
105
|
+
- global allocator override;
|
|
106
|
+
- mutable settings;
|
|
107
|
+
- user callbacks inside C loops.
|
|
108
|
+
|
|
109
|
+
Those require separate ownership and callback-safety contracts before implementation.
|
data/docs/LIMITATIONS.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Limitations
|
|
2
|
+
|
|
3
|
+
`tg_geometry` is a small in-process geometry predicate and geofencing-oriented Index gem. It is not a full GIS system.
|
|
4
|
+
|
|
5
|
+
## Not included
|
|
6
|
+
|
|
7
|
+
The first release does not provide:
|
|
8
|
+
|
|
9
|
+
- geocoding;
|
|
10
|
+
- routing;
|
|
11
|
+
- projections;
|
|
12
|
+
- geodesic distance or area;
|
|
13
|
+
- buffer / union / difference / intersection-result geometry operations;
|
|
14
|
+
- nearest POI index;
|
|
15
|
+
- full PostGIS / GEOS replacement behavior;
|
|
16
|
+
- parsed GeoJSON Feature properties as Ruby Hashes;
|
|
17
|
+
- Line/Ring/Polygon public constructors from Ruby coordinate arrays;
|
|
18
|
+
- user callback APIs;
|
|
19
|
+
- Ractor support claim;
|
|
20
|
+
- no-allocation point query shortcuts;
|
|
21
|
+
- geodesic/projection helpers;
|
|
22
|
+
- mutable coordinate APIs;
|
|
23
|
+
- global TG environment configuration or allocator override APIs.
|
|
24
|
+
|
|
25
|
+
## Planar XY only
|
|
26
|
+
|
|
27
|
+
TG works in planar XY coordinates. If users pass lon/lat, area, length, and perimeter concepts are in input coordinate units, not meters. Real-world distance and area need explicit projection/geodesic tooling outside this first-release core.
|
|
28
|
+
|
|
29
|
+
## Boundary semantics
|
|
30
|
+
|
|
31
|
+
Geofencing defaults to `predicate: :covers` because boundary points should count as inside. `predicate: :contains` is stricter and may exclude boundary points.
|
|
32
|
+
|
|
33
|
+
## Point query performance
|
|
34
|
+
|
|
35
|
+
The current implementation allocates a temporary TG point geometry per point query. This is intentional for exact `covers` and `contains` semantics. A no-allocation point path can be added later only after tests prove equivalent boundary behavior and benchmarks prove value.
|
|
36
|
+
|
|
37
|
+
## Build peak memory
|
|
38
|
+
|
|
39
|
+
`TG::Geometry::Index.build` is atomic and immutable. During reload, memory peak can include both the old Index and the new Index, plus original Ruby entry arrays and temporary build state. This is deliberate: exception safety and read-only concurrency are more important than streaming mutation in the first release.
|
|
40
|
+
|
|
41
|
+
## IDs are returned by reference
|
|
42
|
+
|
|
43
|
+
Index query methods return the same Ruby id objects stored in entries. They are not duplicated, frozen, stringified, or copied. If an id object is mutable, user code owns that mutability risk.
|
|
44
|
+
|
|
45
|
+
## Windows
|
|
46
|
+
|
|
47
|
+
Windows is not supported in the first release. The intended first-release platforms are Linux and macOS on x86_64 and arm64.
|
|
48
|
+
|
|
49
|
+
## Expansion limitations
|
|
50
|
+
|
|
51
|
+
No automatic strategy resolver is enabled in the first public release. For unusual datasets, especially heavily overlapping zones or workloads dominated by first-entry hits, choose `:flat` or `:rtree` explicitly after benchmarking.
|
|
52
|
+
|
|
53
|
+
`TG::Geometry::Registry` is application sugar, not a distributed registry. It has no Redis dependency, no background reload thread, and no hidden global singleton.
|
|
54
|
+
|
|
55
|
+
`TG::Geometry::ActiveRecordSource` is optional Ruby helper code. It does not install Rails reload hooks, generators, or background jobs.
|
|
56
|
+
|
|
57
|
+
`TG::Geometry::Geom#extra_json` returns raw copied JSON text. It does not parse properties and does not expose Feature child objects. Z/M metadata is readable and point Z/M constructors exist, but broad Line/Ring/Polygon/Multi* construction remains out of scope until a separate ownership model is specified.
|
|
58
|
+
|
|
59
|
+
## Expansion Blocks F-H
|
|
60
|
+
|
|
61
|
+
Callback/search APIs, no-allocation point query optimization, and geodesic/projection helpers remain OPEN QUESTION scope. See `docs/EXPANSION_E_TO_H_STATUS.md`.
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# Low-level geometry wrappers
|
|
2
|
+
|
|
3
|
+
This document covers Expansion Block E.
|
|
4
|
+
|
|
5
|
+
## Scope
|
|
6
|
+
|
|
7
|
+
The first low-level API exposes borrowed read-only views over TG child types:
|
|
8
|
+
|
|
9
|
+
- `TG::Geometry::Line`
|
|
10
|
+
- `TG::Geometry::Ring`
|
|
11
|
+
- `TG::Geometry::Polygon`
|
|
12
|
+
- `TG::Geometry::Segment`
|
|
13
|
+
|
|
14
|
+
These wrappers are created only from `TG::Geometry::Geom`, `TG::Geometry::Polygon`, `TG::Geometry::Line`, and `TG::Geometry::Ring` methods. Public `.allocate` is disabled for all four classes.
|
|
15
|
+
|
|
16
|
+
## Ownership model
|
|
17
|
+
|
|
18
|
+
TG child accessors return borrowed pointers owned by the parent `struct tg_geom`. The Line/Ring/Polygon Ruby wrappers therefore store:
|
|
19
|
+
|
|
20
|
+
- `geom_owner`: the original `TG::Geometry::Geom` Ruby object;
|
|
21
|
+
- a borrowed `const struct tg_line *`, `const struct tg_ring *`, or `const struct tg_poly *` pointer.
|
|
22
|
+
|
|
23
|
+
The wrappers mark `geom_owner` with `rb_gc_mark_movable` and update it in `dcompact` with `rb_gc_location`. They do not call `tg_line_free`, `tg_ring_free`, or `tg_poly_free` for borrowed children.
|
|
24
|
+
|
|
25
|
+
`TG::Geometry::Segment` is a value wrapper over a copied `struct tg_segment`, so it does not store `geom_owner` and does not free any TG-owned pointer.
|
|
26
|
+
|
|
27
|
+
Allocator pairs:
|
|
28
|
+
|
|
29
|
+
Resource | Allocator | Deallocator | Owner | Notes
|
|
30
|
+
--- | --- | --- | --- | ---
|
|
31
|
+
`tg_line_wrapper_t` | `TypedData_Make_Struct` / Ruby allocator | `ruby_xfree` | Ruby object | borrowed pointer, marks parent `geom_owner`
|
|
32
|
+
`tg_ring_wrapper_t` | `TypedData_Make_Struct` / Ruby allocator | `ruby_xfree` | Ruby object | borrowed pointer, marks parent `geom_owner`
|
|
33
|
+
`tg_polygon_wrapper_t` | `TypedData_Make_Struct` / Ruby allocator | `ruby_xfree` | Ruby object | borrowed pointer, marks parent `geom_owner`
|
|
34
|
+
`tg_segment_wrapper_t` | `TypedData_Make_Struct` / Ruby allocator | `ruby_xfree` | Ruby object | owns copied `struct tg_segment` value
|
|
35
|
+
TG child pointers | parent `struct tg_geom` | parent `TG::Geometry::Geom` dfree | parent geometry | never freed by child wrappers
|
|
36
|
+
|
|
37
|
+
## Public API
|
|
38
|
+
|
|
39
|
+
### `TG::Geometry::Geom#point`
|
|
40
|
+
|
|
41
|
+
Returns `[x, y]` for point geometries. Returns `nil` for non-point geometries.
|
|
42
|
+
|
|
43
|
+
### `TG::Geometry::Geom#line`
|
|
44
|
+
|
|
45
|
+
Returns a frozen `TG::Geometry::Line` for LineString geometries. Returns `nil` otherwise.
|
|
46
|
+
|
|
47
|
+
### `TG::Geometry::Geom#polygon`
|
|
48
|
+
|
|
49
|
+
Returns a frozen `TG::Geometry::Polygon` for Polygon geometries. Returns `nil` otherwise.
|
|
50
|
+
|
|
51
|
+
### `TG::Geometry::Line`
|
|
52
|
+
|
|
53
|
+
Methods:
|
|
54
|
+
|
|
55
|
+
- `bbox -> TG::Geometry::Rect`
|
|
56
|
+
- `num_points -> Integer`
|
|
57
|
+
- `point_at(index) -> [Float, Float]`
|
|
58
|
+
- `points -> Array<[Float, Float]>`
|
|
59
|
+
- `num_segments -> Integer`
|
|
60
|
+
- `segment_at(index) -> TG::Geometry::Segment`
|
|
61
|
+
- `segments -> Array<TG::Geometry::Segment>`
|
|
62
|
+
- `length -> Float`
|
|
63
|
+
- `clockwise? -> Boolean`
|
|
64
|
+
|
|
65
|
+
`length` is measured in input coordinate units. For lon/lat data this is not meters.
|
|
66
|
+
|
|
67
|
+
### `TG::Geometry::Ring`
|
|
68
|
+
|
|
69
|
+
Methods:
|
|
70
|
+
|
|
71
|
+
- `bbox -> TG::Geometry::Rect`
|
|
72
|
+
- `num_points -> Integer`
|
|
73
|
+
- `point_at(index) -> [Float, Float]`
|
|
74
|
+
- `points -> Array<[Float, Float]>`
|
|
75
|
+
- `num_segments -> Integer`
|
|
76
|
+
- `segment_at(index) -> TG::Geometry::Segment`
|
|
77
|
+
- `segments -> Array<TG::Geometry::Segment>`
|
|
78
|
+
- `area -> Float`
|
|
79
|
+
- `perimeter -> Float`
|
|
80
|
+
- `clockwise? -> Boolean`
|
|
81
|
+
- `convex? -> Boolean`
|
|
82
|
+
|
|
83
|
+
`area` and `perimeter` are measured in input coordinate units. For lon/lat data these are not square meters or meters.
|
|
84
|
+
|
|
85
|
+
### `TG::Geometry::Polygon`
|
|
86
|
+
|
|
87
|
+
Methods:
|
|
88
|
+
|
|
89
|
+
- `bbox -> TG::Geometry::Rect`
|
|
90
|
+
- `exterior_ring -> TG::Geometry::Ring`
|
|
91
|
+
- `num_holes -> Integer`
|
|
92
|
+
- `hole_at(index) -> TG::Geometry::Ring`
|
|
93
|
+
- `holes -> Array<TG::Geometry::Ring>`
|
|
94
|
+
- `clockwise? -> Boolean`
|
|
95
|
+
|
|
96
|
+
`hole_at` rejects out-of-range indexes with `TG::Geometry::ArgumentError`.
|
|
97
|
+
|
|
98
|
+
### `TG::Geometry::Segment`
|
|
99
|
+
|
|
100
|
+
Methods:
|
|
101
|
+
|
|
102
|
+
- `a -> [Float, Float]`
|
|
103
|
+
- `b -> [Float, Float]`
|
|
104
|
+
- `points -> [[Float, Float], [Float, Float]]`
|
|
105
|
+
- `bbox -> TG::Geometry::Rect`
|
|
106
|
+
- `intersects?(other_segment) -> Boolean`
|
|
107
|
+
|
|
108
|
+
Segments are copied by value from a line or ring. They do not keep or free borrowed TG pointers.
|
|
109
|
+
|
|
110
|
+
## Error paths
|
|
111
|
+
|
|
112
|
+
- Wrong wrapper type is handled by `TypedData_Get_Struct` and raises `TypeError`.
|
|
113
|
+
- Out-of-range child indexes raise `TG::Geometry::ArgumentError`.
|
|
114
|
+
- Child wrappers are immutable and cannot be constructed directly from Ruby.
|
|
115
|
+
|
|
116
|
+
## Not implemented in this block
|
|
117
|
+
|
|
118
|
+
- Low-level constructors for Ring, Line, or Polygon.
|
|
119
|
+
- Mutable coordinate access.
|
|
120
|
+
- Polygon `area` / `perimeter` convenience aggregation. Ring-level values are exposed first to avoid undocumented assumptions about hole orientation and aggregation semantics.
|
|
121
|
+
- Geodesic length/area.
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# Memory ownership
|
|
2
|
+
|
|
3
|
+
This document describes allocator pairs, ownership, cleanup, and GC accounting for `tg_geometry`.
|
|
4
|
+
|
|
5
|
+
## Ownership table
|
|
6
|
+
|
|
7
|
+
Resource | Allocator | Deallocator | Owner | Notes
|
|
8
|
+
--- | --- | --- | --- | ---
|
|
9
|
+
`tg_index_t` | `TypedData_Make_Struct` / Ruby allocator | `ruby_xfree` | Ruby object | freed in `index_free`
|
|
10
|
+
entries array | `calloc` | `free` | `TG::Geometry::Index` | exact-size, stable after build; accounted in `entries_bytes`
|
|
11
|
+
rtree internals | `tg_rtree_malloc` / plain `malloc` + header | `tg_rtree_free` / plain `free` | rtree | exact `rtree_bytes`; strict non-NULL current owner
|
|
12
|
+
TG geometry owned | `tg_parse_*_ix` | `tg_geom_free` | `TG::Geometry::Index` | `via: :geojson` / `via: :wkb`; accounted in `owned_geom_bytes_total`
|
|
13
|
+
TG geometry borrowed | `TG::Geometry::Geom` wrapper | `TG::Geometry::Geom` dfree | `TG::Geometry::Geom` | Index holds `geom_owner`; Index does not free borrowed geometry
|
|
14
|
+
TG parse error geometry | `tg_parse_*` | `tg_geom_free` | local parse scope | copy error string before free
|
|
15
|
+
Ruby id | Ruby VM | Ruby GC | `TG::Geometry::Index` entry | mark movable + compact
|
|
16
|
+
`geom_owner` | Ruby VM | Ruby GC | `TG::Geometry::Index` entry | mark movable + compact
|
|
17
|
+
`tg_geom_wrapper_t` owned | `TypedData_Make_Struct` | `ruby_xfree` | Ruby object | owns one `struct tg_geom *`; `geom_free` calls `tg_geom_free`
|
|
18
|
+
`tg_geom_wrapper_t` borrowed | `TypedData_Make_Struct` | `ruby_xfree` | Ruby object | borrowed child `struct tg_geom *`; marks/compacts parent `geom_owner`; does not call `tg_geom_free`
|
|
19
|
+
TG geometry inside owned wrapper | `tg_parse_*` / constructor | `tg_geom_free` in `geom_free` | `TG::Geometry::Geom` wrapper | one `tg_geom_free` per parse/constructor
|
|
20
|
+
query point geometry | `tg_geom_new_point` | `tg_geom_free` | local query scope | one point allocation per point query in first release
|
|
21
|
+
match mark buffer | `calloc` | `free` | local query scope | rtree callback writes C marks only; no Ruby objects touched
|
|
22
|
+
`tg_line_wrapper_t` | `TypedData_Make_Struct` / Ruby allocator | `ruby_xfree` | Ruby object | borrowed child pointer; marks/compacts parent `geom_owner`; does not call `tg_line_free`
|
|
23
|
+
`tg_ring_wrapper_t` | `TypedData_Make_Struct` / Ruby allocator | `ruby_xfree` | Ruby object | borrowed child pointer; marks/compacts parent `geom_owner`; does not call `tg_ring_free`
|
|
24
|
+
`tg_polygon_wrapper_t` | `TypedData_Make_Struct` / Ruby allocator | `ruby_xfree` | Ruby object | borrowed child pointer; marks/compacts parent `geom_owner`; does not call `tg_poly_free`
|
|
25
|
+
`tg_segment_wrapper_t` | `TypedData_Make_Struct` / Ruby allocator | `ruby_xfree` | Ruby object | owns a by-value `struct tg_segment`; no borrowed pointer and no TG free call
|
|
26
|
+
TG child pointers | parent `struct tg_geom` | parent `TG::Geometry::Geom` dfree | parent geometry | borrowed by Geom/Line/Ring/Polygon wrappers; never freed directly
|
|
27
|
+
|
|
28
|
+
## GC memory pressure
|
|
29
|
+
|
|
30
|
+
Native memory that is invisible to Ruby object slots is reported through `rb_gc_adjust_memory_usage` and exposed diagnostically through `ObjectSpace.memsize_of` where Ruby asks the data type for native size.
|
|
31
|
+
|
|
32
|
+
Tracked state:
|
|
33
|
+
|
|
34
|
+
- `TG::Geometry::Geom`: `geom_bytes` only for owned wrappers; borrowed child wrappers report only wrapper size;
|
|
35
|
+
- `TG::Geometry::Index`: `entries_bytes`, `owned_geom_bytes_total`, `rtree_bytes`.
|
|
36
|
+
|
|
37
|
+
Every successful `+N` adjustment has one matching `-N` in the relevant dispose/free path. Dispose is idempotent: after freeing, pointers and byte counters are zeroed.
|
|
38
|
+
|
|
39
|
+
## Partial build cleanup
|
|
40
|
+
|
|
41
|
+
`TG::Geometry::Index.build` uses an explicit `initialized` counter. Only fully written entries are marked, compacted, queried, or disposed.
|
|
42
|
+
|
|
43
|
+
Entry write order:
|
|
44
|
+
|
|
45
|
+
1. validate id and value type;
|
|
46
|
+
2. parse or borrow the native geometry into a local variable;
|
|
47
|
+
3. build a complete local `tg_index_entry_t`;
|
|
48
|
+
4. write the local entry into the entries array;
|
|
49
|
+
5. increment `initialized`;
|
|
50
|
+
6. update bbox and byte accounting.
|
|
51
|
+
|
|
52
|
+
If build raises after native allocation starts, `index_dispose` runs immediately. Failed builds do not wait for Ruby GC to eventually clean large partial native state.
|
|
53
|
+
|
|
54
|
+
## Rtree allocator
|
|
55
|
+
|
|
56
|
+
`rtree.c` expects malloc-style allocation callbacks, so the rtree allocator uses plain `malloc` and returns `NULL` on OOM. It must not call `ruby_xmalloc` and must not raise Ruby exceptions from inside rtree callbacks.
|
|
57
|
+
|
|
58
|
+
Each rtree allocation stores this header before the returned pointer:
|
|
59
|
+
|
|
60
|
+
```c
|
|
61
|
+
typedef struct {
|
|
62
|
+
tg_index_t *owner;
|
|
63
|
+
size_t size;
|
|
64
|
+
} tg_rtree_alloc_header_t;
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
The current owner is `_Thread_local`. It is saved before rtree build, set for `rtree_new_with_allocator` and every `rtree_insert`, then restored with `rb_ensure`.
|
|
68
|
+
|
|
69
|
+
Allocator calls use `rb_gc_adjust_memory_usage`, so rtree build and free must run with the GVL held.
|
|
70
|
+
|
|
71
|
+
## Borrowed geometry path
|
|
72
|
+
|
|
73
|
+
For `via: :geom`, the Index does not clone or copy the native geometry. It stores:
|
|
74
|
+
|
|
75
|
+
- `geom_owner = original TG::Geometry::Geom Ruby object`;
|
|
76
|
+
- `geom = borrowed native pointer`;
|
|
77
|
+
- `owned = false`;
|
|
78
|
+
- `geom_bytes = 0`.
|
|
79
|
+
|
|
80
|
+
The Index marks and compacts `geom_owner`, which keeps the owning Ruby wrapper alive after the caller drops its local variable. Borrowed geometry memory is not double-counted in `ObjectSpace.memsize_of(index)`.
|
|
81
|
+
|
|
82
|
+
## Owned geometry path
|
|
83
|
+
|
|
84
|
+
For `via: :geojson` and `via: :wkb`, the Index owns each parsed TG geometry. The Index calls `tg_geom_free` during dispose/free and subtracts exactly the bytes previously added to `owned_geom_bytes_total` and Ruby GC pressure.
|
|
85
|
+
|
|
86
|
+
## Low-level child wrappers
|
|
87
|
+
|
|
88
|
+
Expansion Block E exposes read-only borrowed wrappers for selected TG child types. `TG::Geometry::Line`, `TG::Geometry::Ring`, and `TG::Geometry::Polygon` keep the original parent `TG::Geometry::Geom` Ruby object in `geom_owner`. Their GC callbacks use `rb_gc_mark_movable` and `rb_gc_location`, matching the Index borrowed-geometry model.
|
|
89
|
+
|
|
90
|
+
Expansion Block J extends this model to borrowed `TG::Geometry::Geom` wrappers returned from GeometryCollection accessors. A borrowed `Geom` stores `owned = false`, `geom_bytes = 0`, and a `geom_owner` reference to the parent wrapper. Its free path only releases the Ruby wrapper struct; it never calls `tg_geom_free` on the borrowed child pointer. This keeps `ObjectSpace.memsize_of` from double-counting parent-owned native geometry.
|
|
91
|
+
|
|
92
|
+
`TG::Geometry::Segment` is different: it stores a `struct tg_segment` by value, not a borrowed pointer. It has no parent owner to mark and no TG deallocator to call.
|
|
93
|
+
|
|
94
|
+
Line/Ring/Polygon wrappers own only their small Ruby-allocated wrapper structs. They do not own the underlying `const struct tg_line *`, `const struct tg_ring *`, or `const struct tg_poly *`. Cleanup therefore only calls `ruby_xfree` for the wrapper; the parent `TG::Geometry::Geom` remains responsible for the single `tg_geom_free`.
|
data/docs/RACTOR.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Ractor investigation
|
|
2
|
+
|
|
3
|
+
This document covers Expansion Block I.
|
|
4
|
+
|
|
5
|
+
## Current result
|
|
6
|
+
|
|
7
|
+
`tg_geometry` does not claim Ractor support.
|
|
8
|
+
|
|
9
|
+
The current native wrappers are frozen and immutable for normal Ruby thread reads, but they are not Ractor-shareable Ruby objects. The test suite records this boundary with `Ractor.shareable?` and `Ractor.make_shareable` checks for:
|
|
10
|
+
|
|
11
|
+
- `TG::Geometry::Geom`
|
|
12
|
+
- `TG::Geometry::Rect`
|
|
13
|
+
- `TG::Geometry::Index`
|
|
14
|
+
|
|
15
|
+
This is an explicit unsupported boundary, not a partial support claim.
|
|
16
|
+
|
|
17
|
+
## Shareability rules
|
|
18
|
+
|
|
19
|
+
Current rule for public docs and code comments:
|
|
20
|
+
|
|
21
|
+
- Normal Ruby thread read-only use is supported by immutable object design and tests.
|
|
22
|
+
- Ractor support is not supported and not advertised.
|
|
23
|
+
- Do not use `TG::Geometry::Geom`, `TG::Geometry::Rect`, `TG::Geometry::Index`, or borrowed low-level wrappers as cross-Ractor shareable objects.
|
|
24
|
+
- Do not add Ractor-specific code paths without a new explicit design and tests.
|
|
25
|
+
|
|
26
|
+
## Why no support claim is made
|
|
27
|
+
|
|
28
|
+
The extension stores native pointers and Ruby `VALUE` references inside TypedData wrappers. The current contract validates GC marking, compaction, ownership, and normal thread read-only access. It does not define or validate cross-Ractor transfer/share semantics for those wrappers.
|
|
29
|
+
|
|
30
|
+
## Required before changing this status
|
|
31
|
+
|
|
32
|
+
Before any future Ractor support claim:
|
|
33
|
+
|
|
34
|
+
1. Define shareability rules for owned and borrowed `TG::Geometry::Geom` wrappers.
|
|
35
|
+
2. Define shareability rules for `TG::Geometry::Index` entries and id `VALUE`s.
|
|
36
|
+
3. Define behavior for borrowed Line/Ring/Polygon/Segment wrappers.
|
|
37
|
+
4. Add Ractor tests that pass on the supported Ruby matrix.
|
|
38
|
+
5. Document whether objects are shareable directly, require duplication, or are unsupported.
|
|
39
|
+
|
|
40
|
+
Until those items exist, Ractor remains unsupported.
|
data/docs/REGISTRY.md
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Registry
|
|
2
|
+
|
|
3
|
+
`TG::Geometry::Registry` is Ruby-side application sugar over immutable `TG::Geometry::Index`.
|
|
4
|
+
|
|
5
|
+
It does not add a mutable native index. `reload!` always builds a new Index and then swaps a Ruby reference:
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
class DeliveryZones < TG::Geometry::Registry
|
|
9
|
+
source do
|
|
10
|
+
[
|
|
11
|
+
[:zone_a, '{"type":"Polygon","coordinates":[[[0,0],[10,0],[10,10],[0,10],[0,0]]]}']
|
|
12
|
+
]
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
registry = DeliveryZones.new(via: :geojson, strategy: :rtree)
|
|
17
|
+
registry.reload!
|
|
18
|
+
registry.find_covering(5, 5)
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Readers capture the current immutable Index. An active reader can continue using the old Index while `reload!` builds and swaps a new one.
|
|
22
|
+
|
|
23
|
+
## API
|
|
24
|
+
|
|
25
|
+
- `source { ... }` on a subclass defines the entry source.
|
|
26
|
+
- `index_options(...)` on a subclass sets default Index options.
|
|
27
|
+
- `reload!` builds and swaps a new Index.
|
|
28
|
+
- `find_covering`, `covering_ids`, `intersecting_rect`, and `covering_ids_batch_packed` delegate to the current Index.
|
|
29
|
+
- `index`, `size`, `bbox`, and `loaded?` expose current state.
|
|
30
|
+
|
|
31
|
+
The source must return the strict first-release entry format:
|
|
32
|
+
|
|
33
|
+
```ruby
|
|
34
|
+
[[id, object], [id, object], ...]
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
No Redis dependency, no Rails dependency, no global native singleton, and no in-place Index mutation are introduced.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# First public release checklist
|
|
2
|
+
|
|
3
|
+
This checklist is intentionally conservative. Do not publish until every required item is satisfied.
|
|
4
|
+
|
|
5
|
+
## Required state before release
|
|
6
|
+
|
|
7
|
+
- [ ] All release-core specs pass locally.
|
|
8
|
+
- [ ] GC.stress specs pass.
|
|
9
|
+
- [ ] GC.compact specs pass.
|
|
10
|
+
- [ ] ASAN status is documented.
|
|
11
|
+
- [ ] Valgrind status is documented.
|
|
12
|
+
- [ ] Benchmark scripts exist and have been run for the full scenario matrix.
|
|
13
|
+
- [ ] README and docs are complete.
|
|
14
|
+
- [ ] Gem metadata is final:
|
|
15
|
+
- gem name: `tg_geometry`;
|
|
16
|
+
- author: `Roman Haydarov`;
|
|
17
|
+
- email: `romnhajdarov@gmail.com`;
|
|
18
|
+
- repository: `https://github.com/roman-haidarov/tg_geometry`.
|
|
19
|
+
- [ ] Vendored TG and rtree versions are pinned in `VERSION` files.
|
|
20
|
+
- [ ] Upstream license files are included under `ext/tg_geometry/vendor/*/LICENSE`.
|
|
21
|
+
- [ ] `CHANGELOG.md` exists.
|
|
22
|
+
|
|
23
|
+
## Do not release if any of these are true
|
|
24
|
+
|
|
25
|
+
- [ ] Any OPEN QUESTION affects correctness.
|
|
26
|
+
- [ ] Any memory accounting path is approximate.
|
|
27
|
+
- [ ] Any failed build path waits for GC instead of immediate dispose.
|
|
28
|
+
- [ ] `strategy: :auto` is enabled without benchmark-derived threshold.
|
|
29
|
+
- [ ] Public API differs from the contract without Roman approval.
|
|
30
|
+
- [ ] Ractor support is claimed.
|
|
31
|
+
- [ ] Performance claims are present without project-owned benchmark output.
|
|
32
|
+
|
|
33
|
+
## OPEN QUESTION: ASAN setup
|
|
34
|
+
|
|
35
|
+
ASAN setup is not fully specified in the contract and the worker must not research it independently. The repository contains a placeholder CI job that fails with an explicit message until Roman approves the exact ASAN approach.
|
|
36
|
+
|
|
37
|
+
## OPEN QUESTION: Valgrind setup
|
|
38
|
+
|
|
39
|
+
Valgrind setup can vary by CI image and Ruby build. The repository contains a placeholder CI job that installs/runs nothing by default and asks for approval before adding a final Valgrind configuration.
|