tg_geometry 0.2.0 → 0.3.1
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 +4 -4
- data/CHANGELOG.md +46 -0
- data/README.md +107 -14
- data/benchmark/_support.rb +450 -5
- data/benchmark/batch_packed_vs_loop.rb +38 -6
- data/benchmark/ewkb_roundtrip.rb +38 -0
- data/benchmark/falcon_concurrency.rb +17 -10
- data/benchmark/feature_source.rb +49 -23
- data/benchmark/flat_vs_rtree.rb +53 -11
- data/benchmark/geom_query.rb +43 -0
- data/benchmark/gvl_threshold.rb +10 -11
- data/benchmark/nearest_segment.rb +24 -0
- data/benchmark/objectspace_memsize.rb +14 -1
- data/benchmark/parse_throughput.rb +13 -10
- data/benchmark/rss_stability.rb +35 -24
- data/docs/GEOMETRY_QUERIES.md +23 -0
- data/docs/LIMITATIONS.md +12 -10
- data/docs/NEAREST_SEGMENT.md +17 -0
- data/docs/SRID_AND_EWKB.md +23 -0
- data/ext/tg_geometry/tg_geometry_ext.c +1234 -27
- data/lib/tg/geometry/active_record_source.rb +16 -34
- data/lib/tg/geometry/active_record_type.rb +61 -0
- data/lib/tg/geometry/registry.rb +17 -68
- data/lib/tg/geometry/version.rb +1 -1
- data/lib/tg/geometry.rb +85 -0
- data/spec/active_record_type_spec.rb +45 -0
- data/spec/constructors_spec.rb +104 -0
- data/spec/fixtures/feature_source/invalid_geometry_middle.geojson +8 -0
- data/spec/fixtures/feature_source/malformed_json.geojson +1 -0
- data/spec/fixtures/feature_source/mixed_geometry_types.geojson +8 -0
- data/spec/fixtures/feature_source/osm_like_feature_collection.geojson +10 -0
- data/spec/fixtures/feature_source/properties_null_missing.geojson +7 -0
- data/spec/fixtures/feature_source/simple_feature_collection.geojson +15 -0
- data/spec/fixtures/postgis/README.md +16 -0
- data/spec/fixtures/postgis/boundary_point_cases.geojson +83 -0
- data/spec/fixtures/postgis/multipolygon_large.ewkb +0 -0
- data/spec/fixtures/postgis/point_4326.ewkb +0 -0
- data/spec/fixtures/postgis/polygon_3857.ewkb +0 -0
- data/spec/fixtures/postgis/polygon_4326_simple.ewkb +0 -0
- data/spec/fixtures/postgis/polygon_4326_with_hole.ewkb +0 -0
- data/spec/index_geom_query_spec.rb +68 -0
- data/spec/keyword_validation_spec.rb +31 -0
- data/spec/nearest_segment_spec.rb +62 -0
- data/spec/postgis_fixtures_spec.rb +68 -0
- data/spec/srid_spec.rb +43 -0
- data/spec/to_ewkb_spec.rb +37 -0
- metadata +50 -2
|
@@ -3,25 +3,57 @@
|
|
|
3
3
|
require_relative "_support"
|
|
4
4
|
|
|
5
5
|
TGGeometryBench.say_header("batch_packed_vs_loop")
|
|
6
|
-
|
|
6
|
+
points_per_batch = TGGeometryBench.env_integer("TGEOMETRY_BENCH_BATCH_POINTS", 1_000, min: 1)
|
|
7
7
|
|
|
8
8
|
%i[compact long_thin overlapping].each do |kind|
|
|
9
9
|
TGGeometryBench.sizes.each do |size|
|
|
10
10
|
entries = TGGeometryBench.entries_for(kind, size)
|
|
11
|
-
points =
|
|
11
|
+
points = TGGeometryBench.repeated_points(kind, points_per_batch)
|
|
12
12
|
packed = TGGeometryBench.packed_points(points)
|
|
13
13
|
|
|
14
14
|
%i[flat rtree].each do |strategy|
|
|
15
15
|
index = TGGeometryBench.build_index(entries, strategy: strategy)
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
scalar = TGGeometryBench.measure_counted(
|
|
18
|
+
initial_iterations: TGGeometryBench.initial_iterations(50),
|
|
19
|
+
operations_per_iteration: points.length
|
|
20
|
+
) do |iterations|
|
|
21
|
+
iterations.times do
|
|
22
|
+
points.each { |lon, lat| index.find_covering(lon, lat) }
|
|
23
|
+
end
|
|
19
24
|
end
|
|
20
|
-
|
|
25
|
+
|
|
26
|
+
batch = TGGeometryBench.measure_counted(
|
|
27
|
+
initial_iterations: TGGeometryBench.initial_iterations(50),
|
|
28
|
+
operations_per_iteration: points.length
|
|
29
|
+
) do |iterations|
|
|
21
30
|
iterations.times { index.covering_ids_batch_packed(packed) }
|
|
22
31
|
end
|
|
23
32
|
|
|
24
|
-
|
|
33
|
+
TGGeometryBench.report(
|
|
34
|
+
"batch_packed_vs_loop",
|
|
35
|
+
{
|
|
36
|
+
kind: kind,
|
|
37
|
+
n: size,
|
|
38
|
+
strategy: strategy,
|
|
39
|
+
mode: :scalar_loop,
|
|
40
|
+
points_per_batch: points.length,
|
|
41
|
+
batches_per_sec: scalar[:ops_per_sec] / points.length
|
|
42
|
+
},
|
|
43
|
+
stats: scalar
|
|
44
|
+
)
|
|
45
|
+
TGGeometryBench.report(
|
|
46
|
+
"batch_packed_vs_loop",
|
|
47
|
+
{
|
|
48
|
+
kind: kind,
|
|
49
|
+
n: size,
|
|
50
|
+
strategy: strategy,
|
|
51
|
+
mode: :packed_batch,
|
|
52
|
+
points_per_batch: points.length,
|
|
53
|
+
batches_per_sec: batch[:ops_per_sec] / points.length
|
|
54
|
+
},
|
|
55
|
+
stats: batch
|
|
56
|
+
)
|
|
25
57
|
end
|
|
26
58
|
end
|
|
27
59
|
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "_support"
|
|
4
|
+
|
|
5
|
+
TGGeometryBench.say_header("ewkb_roundtrip")
|
|
6
|
+
|
|
7
|
+
geom = TG::Geometry.polygon([[0, 0], [10, 0], [10, 10], [0, 10], [0, 0]], srid: 4326)
|
|
8
|
+
ewkb = geom.to_ewkb
|
|
9
|
+
|
|
10
|
+
stats = TGGeometryBench.measure_counted(initial_iterations: TGGeometryBench.initial_iterations(5_000)) do |iterations|
|
|
11
|
+
iterations.times { TG::Geometry.parse_wkb(ewkb).to_ewkb }
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
TGGeometryBench.report(
|
|
15
|
+
"ewkb_roundtrip",
|
|
16
|
+
{ library: :tg_geometry, operation: :parse_wkb_to_ewkb, payload_bytes: ewkb.bytesize },
|
|
17
|
+
stats: stats
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
if ENV["WITH_RGEO"]
|
|
21
|
+
begin
|
|
22
|
+
require "rgeo"
|
|
23
|
+
factory = RGeo::Cartesian.factory(srid: 4326)
|
|
24
|
+
rgeo_geom = factory.parse_wkt("POLYGON ((0 0, 10 0, 10 10, 0 10, 0 0))")
|
|
25
|
+
|
|
26
|
+
rgeo_stats = TGGeometryBench.measure_counted(initial_iterations: TGGeometryBench.initial_iterations(5_000)) do |iterations|
|
|
27
|
+
iterations.times { factory.parse_wkt(rgeo_geom.as_text) }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
TGGeometryBench.report(
|
|
31
|
+
"ewkb_roundtrip",
|
|
32
|
+
{ library: :rgeo, operation: :wkt_parse, payload_bytes: rgeo_geom.as_text.bytesize },
|
|
33
|
+
stats: rgeo_stats
|
|
34
|
+
)
|
|
35
|
+
rescue LoadError
|
|
36
|
+
warn "rgeo is not installed"
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -3,23 +3,30 @@
|
|
|
3
3
|
require_relative "_support"
|
|
4
4
|
|
|
5
5
|
TGGeometryBench.say_header("falcon_concurrency")
|
|
6
|
-
|
|
7
|
-
puts "Falcon/Async behavior remains an Pending decision until Roman approves a dedicated dependency/setup."
|
|
6
|
+
TGGeometryBench.report("falcon_concurrency_note", note: "thread_read_baseline_only_no_falcon_dependency")
|
|
8
7
|
|
|
9
|
-
|
|
8
|
+
entries_count = TGGeometryBench.env_integer("TGEOMETRY_BENCH_ENTRIES", 1_000, min: 1)
|
|
9
|
+
threads = TGGeometryBench.env_integer("TGEOMETRY_BENCH_THREADS", 4, min: 1)
|
|
10
|
+
entries = TGGeometryBench.compact_entries(entries_count)
|
|
10
11
|
index = TGGeometryBench.build_index(entries, strategy: :rtree)
|
|
11
|
-
|
|
12
|
-
iterations = TGGeometryBench.iterations(10_000)
|
|
12
|
+
points = TGGeometryBench.points_for(:compact)
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
stats = TGGeometryBench.measure_counted(
|
|
15
|
+
initial_iterations: TGGeometryBench.initial_iterations(2_000),
|
|
16
|
+
operations_per_iteration: threads
|
|
17
|
+
) do |iterations_per_thread|
|
|
18
|
+
threads.times.map do |thread_index|
|
|
16
19
|
Thread.new do
|
|
17
|
-
|
|
18
|
-
lon, lat =
|
|
20
|
+
iterations_per_thread.times do |i|
|
|
21
|
+
lon, lat = points[(i + thread_index) % points.length]
|
|
19
22
|
index.find_covering(lon, lat)
|
|
20
23
|
end
|
|
21
24
|
end
|
|
22
25
|
end.each(&:join)
|
|
23
26
|
end
|
|
24
27
|
|
|
25
|
-
|
|
28
|
+
TGGeometryBench.report(
|
|
29
|
+
"falcon_concurrency",
|
|
30
|
+
{ threads: threads, entries: entries_count, strategy: :rtree, operation: :find_covering },
|
|
31
|
+
stats: stats
|
|
32
|
+
)
|
data/benchmark/feature_source.rb
CHANGED
|
@@ -40,7 +40,7 @@ end
|
|
|
40
40
|
|
|
41
41
|
TGGeometryBench.say_header("feature_source")
|
|
42
42
|
|
|
43
|
-
sizes =
|
|
43
|
+
sizes = TGGeometryBench.full? ? [100, 1_000, 10_000, 50_000] : [100, 1_000, 10_000]
|
|
44
44
|
|
|
45
45
|
sizes.each do |size|
|
|
46
46
|
json = FeatureSourceBenchData.feature_collection(size)
|
|
@@ -54,38 +54,64 @@ sizes.each do |size|
|
|
|
54
54
|
direct_index = nil
|
|
55
55
|
roundtrip_index = nil
|
|
56
56
|
|
|
57
|
-
|
|
58
|
-
ruby_entries = FeatureSourceBenchData.ruby_json_parse_entries(path)
|
|
57
|
+
ruby = TGGeometryBench.measure_counted(initial_iterations: 1, min_seconds: 0.30) do |iterations|
|
|
58
|
+
iterations.times { ruby_entries = FeatureSourceBenchData.ruby_json_parse_entries(path) }
|
|
59
59
|
end
|
|
60
60
|
|
|
61
|
-
|
|
62
|
-
|
|
61
|
+
read_entries = TGGeometryBench.measure_counted(initial_iterations: 1, min_seconds: 0.30) do |iterations|
|
|
62
|
+
iterations.times do
|
|
63
|
+
feature_entries = TG::Geometry::FeatureSource.read_entries_file(path, id: ["properties", "@id"])
|
|
64
|
+
end
|
|
63
65
|
end
|
|
64
66
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
+
read_features = TGGeometryBench.measure_counted(initial_iterations: 1, min_seconds: 0.30) do |iterations|
|
|
68
|
+
iterations.times do
|
|
69
|
+
feature_rows = TG::Geometry::FeatureSource.read_features_file(path, id: ["properties", "@id"])
|
|
70
|
+
end
|
|
67
71
|
end
|
|
68
72
|
|
|
69
|
-
|
|
70
|
-
|
|
73
|
+
direct_index_stat = TGGeometryBench.measure_counted(initial_iterations: 1, min_seconds: 0.30) do |iterations|
|
|
74
|
+
iterations.times do
|
|
75
|
+
direct_index = TG::Geometry::FeatureSource.build_index_file(path, id: ["properties", "@id"], strategy: :rtree)
|
|
76
|
+
end
|
|
71
77
|
end
|
|
72
78
|
|
|
73
|
-
|
|
74
|
-
|
|
79
|
+
# Ensure the roundtrip benchmark does not accidentally include read_entries.
|
|
80
|
+
feature_entries ||= TG::Geometry::FeatureSource.read_entries_file(path, id: ["properties", "@id"])
|
|
81
|
+
|
|
82
|
+
roundtrip_index_stat = TGGeometryBench.measure_counted(initial_iterations: 1, min_seconds: 0.30) do |iterations|
|
|
83
|
+
iterations.times do
|
|
84
|
+
roundtrip_index = TG::Geometry::Index.build(feature_entries, via: :geojson, strategy: :rtree)
|
|
85
|
+
end
|
|
75
86
|
end
|
|
76
87
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
ruby_entries.length,
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
88
|
+
read_plus_roundtrip_sec = read_entries[:median_sec] + roundtrip_index_stat[:median_sec]
|
|
89
|
+
read_plus_roundtrip_ops_per_sec = read_plus_roundtrip_sec.positive? ? read_entries[:iterations] / read_plus_roundtrip_sec : 0.0
|
|
90
|
+
|
|
91
|
+
common = {
|
|
92
|
+
n: size,
|
|
93
|
+
json_bytes: json.bytesize,
|
|
94
|
+
ruby_entries: ruby_entries.length,
|
|
95
|
+
feature_entries: feature_entries.length,
|
|
96
|
+
feature_rows: feature_rows.length,
|
|
97
|
+
direct_size: direct_index.size,
|
|
98
|
+
roundtrip_size: roundtrip_index.size,
|
|
99
|
+
rss_kb: TGGeometryBench.rss_kb
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
TGGeometryBench.report("feature_source", common.merge(mode: :ruby_json_parse_entries), stats: ruby)
|
|
103
|
+
TGGeometryBench.report("feature_source", common.merge(mode: :read_entries_file), stats: read_entries)
|
|
104
|
+
TGGeometryBench.report("feature_source", common.merge(mode: :read_features_file), stats: read_features)
|
|
105
|
+
TGGeometryBench.report("feature_source", common.merge(mode: :build_index_direct), stats: direct_index_stat)
|
|
106
|
+
TGGeometryBench.report("feature_source", common.merge(mode: :build_index_from_prepared_entries), stats: roundtrip_index_stat)
|
|
107
|
+
TGGeometryBench.report(
|
|
108
|
+
"feature_source_end_to_end",
|
|
109
|
+
common.merge(
|
|
110
|
+
mode: :read_entries_plus_build_index,
|
|
111
|
+
median_sec: read_plus_roundtrip_sec,
|
|
112
|
+
ops_per_sec: read_plus_roundtrip_ops_per_sec
|
|
113
|
+
)
|
|
114
|
+
)
|
|
89
115
|
ensure
|
|
90
116
|
file.close!
|
|
91
117
|
end
|
data/benchmark/flat_vs_rtree.rb
CHANGED
|
@@ -3,25 +3,67 @@
|
|
|
3
3
|
require_relative "_support"
|
|
4
4
|
|
|
5
5
|
TGGeometryBench.say_header("flat_vs_rtree")
|
|
6
|
-
iterations = TGGeometryBench.iterations(1_000)
|
|
7
6
|
|
|
8
7
|
%i[compact long_thin overlapping].each do |kind|
|
|
9
8
|
TGGeometryBench.sizes.each do |size|
|
|
10
9
|
entries = TGGeometryBench.entries_for(kind, size)
|
|
11
10
|
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
flat = nil
|
|
12
|
+
rtree = nil
|
|
14
13
|
|
|
15
|
-
TGGeometryBench.
|
|
16
|
-
|
|
17
|
-
rtree_time = Benchmark.realtime { iterations.times { @rtree.find_covering(lon, lat) } }
|
|
18
|
-
puts "kind=#{kind} n=#{size} query=point lon=#{lon} lat=#{lat} flat_sec=%.6f rtree_sec=%.6f flat_qps=%.2f rtree_qps=%.2f build_flat=%.6f build_rtree=%.6f" % [flat_time, rtree_time, iterations / flat_time, iterations / rtree_time, build_flat, build_rtree]
|
|
14
|
+
flat_build = TGGeometryBench.measure_counted(initial_iterations: 1, min_seconds: 0.25) do |iterations|
|
|
15
|
+
iterations.times { flat = TGGeometryBench.build_index(entries, strategy: :flat) }
|
|
19
16
|
end
|
|
17
|
+
rtree_build = TGGeometryBench.measure_counted(initial_iterations: 1, min_seconds: 0.25) do |iterations|
|
|
18
|
+
iterations.times { rtree = TGGeometryBench.build_index(entries, strategy: :rtree) }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Rebuild once outside the build benchmark so query numbers do not depend on
|
|
22
|
+
# the last object produced by calibration/repeats.
|
|
23
|
+
flat = TGGeometryBench.build_index(entries, strategy: :flat)
|
|
24
|
+
rtree = TGGeometryBench.build_index(entries, strategy: :rtree)
|
|
25
|
+
|
|
26
|
+
TGGeometryBench.report("flat_vs_rtree_build", { kind: kind, n: size, strategy: :flat }, stats: flat_build)
|
|
27
|
+
TGGeometryBench.report("flat_vs_rtree_build", { kind: kind, n: size, strategy: :rtree }, stats: rtree_build)
|
|
28
|
+
|
|
29
|
+
TGGeometryBench.points_for(kind).each_with_index do |(lon, lat), point_index|
|
|
30
|
+
flat_stats = TGGeometryBench.measure_counted(initial_iterations: TGGeometryBench.initial_iterations(2_000)) do |iterations|
|
|
31
|
+
iterations.times { flat.find_covering(lon, lat) }
|
|
32
|
+
end
|
|
33
|
+
rtree_stats = TGGeometryBench.measure_counted(initial_iterations: TGGeometryBench.initial_iterations(2_000)) do |iterations|
|
|
34
|
+
iterations.times { rtree.find_covering(lon, lat) }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
TGGeometryBench.report(
|
|
38
|
+
"flat_vs_rtree_point",
|
|
39
|
+
{ kind: kind, n: size, strategy: :flat, point_index: point_index, lon: lon, lat: lat },
|
|
40
|
+
stats: flat_stats
|
|
41
|
+
)
|
|
42
|
+
TGGeometryBench.report(
|
|
43
|
+
"flat_vs_rtree_point",
|
|
44
|
+
{ kind: kind, n: size, strategy: :rtree, point_index: point_index, lon: lon, lat: lat },
|
|
45
|
+
stats: rtree_stats
|
|
46
|
+
)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
TGGeometryBench.rects_for(kind).each_with_index do |rect, rect_index|
|
|
50
|
+
flat_stats = TGGeometryBench.measure_counted(initial_iterations: TGGeometryBench.initial_iterations(200)) do |iterations|
|
|
51
|
+
iterations.times { flat.intersecting_rect(*rect) }
|
|
52
|
+
end
|
|
53
|
+
rtree_stats = TGGeometryBench.measure_counted(initial_iterations: TGGeometryBench.initial_iterations(200)) do |iterations|
|
|
54
|
+
iterations.times { rtree.intersecting_rect(*rect) }
|
|
55
|
+
end
|
|
20
56
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
57
|
+
TGGeometryBench.report(
|
|
58
|
+
"flat_vs_rtree_rect",
|
|
59
|
+
{ kind: kind, n: size, strategy: :flat, rect_index: rect_index, rect: rect },
|
|
60
|
+
stats: flat_stats
|
|
61
|
+
)
|
|
62
|
+
TGGeometryBench.report(
|
|
63
|
+
"flat_vs_rtree_rect",
|
|
64
|
+
{ kind: kind, n: size, strategy: :rtree, rect_index: rect_index, rect: rect },
|
|
65
|
+
stats: rtree_stats
|
|
66
|
+
)
|
|
25
67
|
end
|
|
26
68
|
end
|
|
27
69
|
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "_support"
|
|
4
|
+
|
|
5
|
+
TGGeometryBench.say_header("geom_query")
|
|
6
|
+
|
|
7
|
+
COUNTS = (TGGeometryBench.full? ? [1_000, 10_000, 50_000] : [1_000, 10_000]).freeze
|
|
8
|
+
STRATEGIES = %i[flat rtree].freeze
|
|
9
|
+
|
|
10
|
+
def box(x, y, size = 1.0)
|
|
11
|
+
TG::Geometry.polygon([[x, y], [x + size, y], [x + size, y + size], [x, y + size], [x, y]])
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
COUNTS.each do |count|
|
|
15
|
+
entries = count.times.map do |i|
|
|
16
|
+
x = (i % 100).to_f * 2.0
|
|
17
|
+
y = (i / 100).to_f * 2.0
|
|
18
|
+
[i, box(x, y)]
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
queries = {
|
|
22
|
+
small: box(10.5, 10.5, 2.0),
|
|
23
|
+
large: box(0.5, 0.5, 120.0),
|
|
24
|
+
miss: box(-1_000.0, -1_000.0, 1.0)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
STRATEGIES.each do |strategy|
|
|
28
|
+
index = TG::Geometry::Index.build(entries, via: :geom, strategy: strategy)
|
|
29
|
+
|
|
30
|
+
queries.each do |query_name, query_geom|
|
|
31
|
+
initial = query_name == :large ? 100 : 1_000
|
|
32
|
+
stats = TGGeometryBench.measure_counted(initial_iterations: TGGeometryBench.initial_iterations(initial)) do |iterations|
|
|
33
|
+
iterations.times { index.intersecting_geom_ids(query_geom) }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
TGGeometryBench.report(
|
|
37
|
+
"geom_query",
|
|
38
|
+
{ n: count, strategy: strategy, query: query_name, method: :intersecting_geom_ids },
|
|
39
|
+
stats: stats
|
|
40
|
+
)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
data/benchmark/gvl_threshold.rb
CHANGED
|
@@ -3,12 +3,9 @@
|
|
|
3
3
|
require_relative "_support"
|
|
4
4
|
|
|
5
5
|
TGGeometryBench.say_header("gvl_threshold")
|
|
6
|
-
|
|
7
|
-
puts "This harness records baseline parse wall time only; it does not enable no-GVL execution."
|
|
6
|
+
TGGeometryBench.report("gvl_threshold_note", note: "baseline_only_gvl_is_held_no_public_release_gvl_knob")
|
|
8
7
|
|
|
9
|
-
# Build one valid WKT polygon close to the requested byte size.
|
|
10
|
-
# implementation accidentally benchmarked the same tiny 39-byte polygon for all
|
|
11
|
-
# target sizes because it used Array(...).first.
|
|
8
|
+
# Build one valid WKT polygon close to the requested byte size.
|
|
12
9
|
def polygon_wkt_at_least(target_bytes)
|
|
13
10
|
points_count = [4, target_bytes / 38].max
|
|
14
11
|
|
|
@@ -27,15 +24,17 @@ def polygon_wkt_at_least(target_bytes)
|
|
|
27
24
|
end
|
|
28
25
|
end
|
|
29
26
|
|
|
30
|
-
|
|
31
|
-
iterations = TGGeometryBench.iterations(2_000)
|
|
32
|
-
|
|
33
|
-
sizes.each do |target_bytes|
|
|
27
|
+
[128, 1_024, 16_384, 262_144].each do |target_bytes|
|
|
34
28
|
payload = polygon_wkt_at_least(target_bytes)
|
|
29
|
+
initial = target_bytes >= 262_144 ? 10 : 500
|
|
35
30
|
|
|
36
|
-
|
|
31
|
+
stats = TGGeometryBench.measure_counted(initial_iterations: TGGeometryBench.initial_iterations(initial)) do |iterations|
|
|
37
32
|
iterations.times { TG::Geometry.parse_wkt(payload) }
|
|
38
33
|
end
|
|
39
34
|
|
|
40
|
-
|
|
35
|
+
TGGeometryBench.report(
|
|
36
|
+
"gvl_threshold_parse_wkt",
|
|
37
|
+
{ target_bytes: target_bytes, payload_bytes: payload.bytesize },
|
|
38
|
+
stats: stats
|
|
39
|
+
)
|
|
41
40
|
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "_support"
|
|
4
|
+
|
|
5
|
+
TGGeometryBench.say_header("nearest_segment")
|
|
6
|
+
|
|
7
|
+
[100, 1_000, 10_000].each do |count|
|
|
8
|
+
points = count.times.map do |i|
|
|
9
|
+
angle = 2.0 * Math::PI * i / count
|
|
10
|
+
[Math.cos(angle), Math.sin(angle)]
|
|
11
|
+
end
|
|
12
|
+
points << points.first
|
|
13
|
+
ring = TG::Geometry.polygon(points).polygon.exterior_ring
|
|
14
|
+
|
|
15
|
+
stats = TGGeometryBench.measure_counted(initial_iterations: TGGeometryBench.initial_iterations(10_000)) do |iterations|
|
|
16
|
+
iterations.times { ring.nearest_segment(0.25, 0.33) }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
TGGeometryBench.report(
|
|
20
|
+
"nearest_segment",
|
|
21
|
+
{ segments: ring.num_segments, query: "0.25:0.33" },
|
|
22
|
+
stats: stats
|
|
23
|
+
)
|
|
24
|
+
end
|
|
@@ -8,10 +8,23 @@ TGGeometryBench.say_header("objectspace_memsize")
|
|
|
8
8
|
%i[compact long_thin overlapping].each do |kind|
|
|
9
9
|
TGGeometryBench.sizes.each do |size|
|
|
10
10
|
entries = TGGeometryBench.entries_for(kind, size)
|
|
11
|
+
TGGeometryBench.gc_start
|
|
11
12
|
flat = TGGeometryBench.build_index(entries, strategy: :flat)
|
|
13
|
+
TGGeometryBench.gc_start
|
|
12
14
|
rtree = TGGeometryBench.build_index(entries, strategy: :rtree)
|
|
13
15
|
geom = TG::Geometry.parse_geojson(entries.first.last)
|
|
14
16
|
|
|
15
|
-
|
|
17
|
+
TGGeometryBench.report(
|
|
18
|
+
"objectspace_memsize",
|
|
19
|
+
{
|
|
20
|
+
kind: kind,
|
|
21
|
+
n: size,
|
|
22
|
+
geom_memsize: ObjectSpace.memsize_of(geom),
|
|
23
|
+
flat_memsize: ObjectSpace.memsize_of(flat),
|
|
24
|
+
rtree_memsize: ObjectSpace.memsize_of(rtree),
|
|
25
|
+
rtree_over_flat: ObjectSpace.memsize_of(rtree) - ObjectSpace.memsize_of(flat),
|
|
26
|
+
rss_kb: TGGeometryBench.rss_kb
|
|
27
|
+
}
|
|
28
|
+
)
|
|
16
29
|
end
|
|
17
30
|
end
|
|
@@ -21,18 +21,21 @@ geojson = '{"type":"Polygon","coordinates":[[[0,0],[10,0],[10,10],[0,10],[0,0]]]
|
|
|
21
21
|
wkb = TG::Geometry.parse_wkt(small).to_wkb
|
|
22
22
|
|
|
23
23
|
cases = {
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"
|
|
24
|
+
"wkt_small" => [small, :wkt, 5_000],
|
|
25
|
+
"geojson_small" => [geojson, :geojson, 5_000],
|
|
26
|
+
"wkb_small" => [wkb, :wkb, 5_000],
|
|
27
|
+
"wkt_medium" => [medium, :wkt, 500],
|
|
28
|
+
"wkt_large" => [large, :wkt, 50]
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
cases.each do |name, (payload, format)|
|
|
34
|
-
time = Benchmark.realtime do
|
|
31
|
+
cases.each do |name, (payload, format, initial)|
|
|
32
|
+
stats = TGGeometryBench.measure_counted(initial_iterations: TGGeometryBench.initial_iterations(initial)) do |iterations|
|
|
35
33
|
iterations.times { TG::Geometry.parse(payload, format: format) }
|
|
36
34
|
end
|
|
37
|
-
|
|
35
|
+
|
|
36
|
+
TGGeometryBench.report(
|
|
37
|
+
"parse_throughput",
|
|
38
|
+
{ case: name, format: format, payload_bytes: payload.bytesize },
|
|
39
|
+
stats: stats
|
|
40
|
+
)
|
|
38
41
|
end
|
data/benchmark/rss_stability.rb
CHANGED
|
@@ -4,27 +4,28 @@ require_relative "_support"
|
|
|
4
4
|
|
|
5
5
|
TGGeometryBench.say_header("rss_stability")
|
|
6
6
|
|
|
7
|
-
total_queries =
|
|
8
|
-
rebuilds =
|
|
9
|
-
entries_count =
|
|
10
|
-
max_drift_kb =
|
|
7
|
+
total_queries = TGGeometryBench.env_integer("TGEOMETRY_RSS_QUERIES", 10_000_000, min: 1)
|
|
8
|
+
rebuilds = TGGeometryBench.env_integer("TGEOMETRY_RSS_REBUILDS", 100, min: 1)
|
|
9
|
+
entries_count = TGGeometryBench.env_integer("TGEOMETRY_RSS_ENTRIES", 1_000, min: 1)
|
|
10
|
+
max_drift_kb = TGGeometryBench.env_integer("TGEOMETRY_RSS_MAX_DRIFT_KB", 51_200, min: 0)
|
|
11
11
|
|
|
12
12
|
queries_per_rebuild = (total_queries / rebuilds).clamp(1, total_queries)
|
|
13
13
|
entries = TGGeometryBench.compact_entries(entries_count)
|
|
14
14
|
points = TGGeometryBench.points_for(:compact)
|
|
15
15
|
packed_batch = TGGeometryBench.packed_points(points * 10)
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
TGGeometryBench.gc_start
|
|
18
|
+
TGGeometryBench.gc_start
|
|
19
19
|
start_rss = TGGeometryBench.rss_kb
|
|
20
20
|
peak_rss = start_rss
|
|
21
21
|
samples = []
|
|
22
22
|
|
|
23
23
|
queries_executed = 0
|
|
24
|
-
started_at =
|
|
24
|
+
started_at = TGGeometryBench.monotonic
|
|
25
25
|
|
|
26
26
|
rebuilds.times do |cycle|
|
|
27
|
-
|
|
27
|
+
strategy = cycle.even? ? :flat : :rtree
|
|
28
|
+
index = TGGeometryBench.build_index(entries, strategy: strategy)
|
|
28
29
|
|
|
29
30
|
queries_per_rebuild.times do |q|
|
|
30
31
|
lon, lat = points[(q + cycle) % points.length]
|
|
@@ -39,29 +40,39 @@ rebuilds.times do |cycle|
|
|
|
39
40
|
|
|
40
41
|
index = nil
|
|
41
42
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
43
|
+
next unless (cycle % 10).zero?
|
|
44
|
+
|
|
45
|
+
TGGeometryBench.gc_start
|
|
46
|
+
rss = TGGeometryBench.rss_kb
|
|
47
|
+
peak_rss = [peak_rss, rss].max
|
|
48
|
+
samples << [cycle, queries_executed, rss]
|
|
48
49
|
end
|
|
49
50
|
|
|
50
|
-
|
|
51
|
-
|
|
51
|
+
TGGeometryBench.gc_start
|
|
52
|
+
TGGeometryBench.gc_start
|
|
52
53
|
finish_rss = TGGeometryBench.rss_kb
|
|
53
|
-
elapsed =
|
|
54
|
+
elapsed = TGGeometryBench.monotonic - started_at
|
|
54
55
|
drift_kb = finish_rss - start_rss
|
|
55
56
|
|
|
56
|
-
|
|
57
|
-
"
|
|
58
|
-
|
|
59
|
-
|
|
57
|
+
TGGeometryBench.report(
|
|
58
|
+
"rss_stability",
|
|
59
|
+
{
|
|
60
|
+
queries: queries_executed,
|
|
61
|
+
rebuilds: rebuilds,
|
|
62
|
+
entries: entries_count,
|
|
63
|
+
elapsed_sec: elapsed,
|
|
64
|
+
qps: queries_executed / elapsed,
|
|
65
|
+
start_rss_kb: start_rss,
|
|
66
|
+
peak_rss_kb: peak_rss,
|
|
67
|
+
finish_rss_kb: finish_rss,
|
|
68
|
+
drift_kb: drift_kb,
|
|
69
|
+
max_drift_kb: max_drift_kb,
|
|
70
|
+
sample_count: samples.length
|
|
71
|
+
}
|
|
60
72
|
)
|
|
61
73
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
samples.each { |row| puts " #{row.inspect}" }
|
|
74
|
+
samples.each do |cycle, queries, rss|
|
|
75
|
+
TGGeometryBench.report("rss_stability_sample", cycle: cycle, queries: queries, rss_kb: rss)
|
|
65
76
|
end
|
|
66
77
|
|
|
67
78
|
if max_drift_kb.positive? && drift_kb > max_drift_kb
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Geometry-based index queries
|
|
2
|
+
|
|
3
|
+
v0.3.0 adds geometry query methods to `TG::Geometry::Index`:
|
|
4
|
+
|
|
5
|
+
| Method | Predicate direction | Boundary semantics |
|
|
6
|
+
| --- | --- | --- |
|
|
7
|
+
| `intersecting_geom_ids(query)` | `stored_geom` intersects `query` | any intersection |
|
|
8
|
+
| `covering_geom_ids(query)` | `stored_geom` covers `query` | boundary included |
|
|
9
|
+
| `containing_geom_ids(query)` | `stored_geom` contains `query` | strict interior; boundary excluded |
|
|
10
|
+
|
|
11
|
+
The direction is always from the stored geometry to the query geometry. For example, `covering_geom_ids(query)` asks which indexed geometries cover the query.
|
|
12
|
+
|
|
13
|
+
The `predicate:` option on `Index.build` affects only the legacy point-based methods:
|
|
14
|
+
|
|
15
|
+
- `find_covering(x, y)`
|
|
16
|
+
- `covering_ids(x, y)`
|
|
17
|
+
- `covering_ids_batch_packed(packed_doubles)`
|
|
18
|
+
|
|
19
|
+
It does not affect `intersecting_geom_ids`, `covering_geom_ids`, or `containing_geom_ids`.
|
|
20
|
+
|
|
21
|
+
Results are arrays of ids in insertion order. Duplicate ids are preserved if duplicate ids were inserted.
|
|
22
|
+
|
|
23
|
+
In v0.3.0 these operations run under the GVL. The heavy C phase is structured without Ruby API calls so it can be made no-GVL-safe later after benchmarking, but there is no public `release_gvl:` knob.
|
data/docs/LIMITATIONS.md
CHANGED
|
@@ -4,16 +4,18 @@
|
|
|
4
4
|
|
|
5
5
|
Not included:
|
|
6
6
|
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
- Ractor support
|
|
15
|
-
-
|
|
16
|
-
-
|
|
7
|
+
- Geodesic / Haversine distance;
|
|
8
|
+
- Projection / reprojection;
|
|
9
|
+
- Buffer / union / difference / convex hull;
|
|
10
|
+
- Index nearest_ids (KNN);
|
|
11
|
+
- GeoBIN bbox helpers;
|
|
12
|
+
- Streaming FeatureSource;
|
|
13
|
+
- Index serialization / mmap;
|
|
14
|
+
- Ractor support;
|
|
15
|
+
- Windows / JRuby;
|
|
16
|
+
- Write-side ActiveRecordType / AR scopes / migrations;
|
|
17
|
+
- Z/M variants of array constructors;
|
|
18
|
+
- Public `release_gvl:` option.
|
|
17
19
|
|
|
18
20
|
TG works in planar XY coordinates. If lon/lat coordinates are passed in, length, area, and perimeter-style values are in input coordinate units, not meters.
|
|
19
21
|
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Nearest segment
|
|
2
|
+
|
|
3
|
+
`Line#nearest_segment(x, y)` and `Ring#nearest_segment(x, y)` compute the nearest segment to a point in planar XY space.
|
|
4
|
+
|
|
5
|
+
```ruby
|
|
6
|
+
nearest = ring.nearest_segment(x, y)
|
|
7
|
+
nearest.segment # TG::Geometry::Segment
|
|
8
|
+
nearest.index # Integer segment index in the parent line/ring
|
|
9
|
+
nearest.distance # Float
|
|
10
|
+
nearest.point # [x, y] projection onto the segment
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Distance is Euclidean distance in input coordinate units. It is not meters unless the input coordinate system is already meters.
|
|
14
|
+
|
|
15
|
+
Degenerate segments (`a == b`) are handled as point distance. The projection point for a degenerate segment is the segment endpoint.
|
|
16
|
+
|
|
17
|
+
When several segments have the same distance, tie-break behaviour follows tg iteration order. Code should not rely on a specific equal-distance segment.
|