tg_geometry 0.3.0 → 0.3.2
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 +30 -0
- data/README.md +41 -1
- data/benchmark/_support.rb +457 -5
- data/benchmark/batch_packed_vs_loop.rb +38 -6
- data/benchmark/distance_memory_accounting.rb +46 -0
- data/benchmark/distance_point_geom.rb +49 -0
- data/benchmark/distance_within_radius.rb +75 -0
- data/benchmark/ewkb_roundtrip.rb +18 -9
- 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 +19 -9
- data/benchmark/gvl_threshold.rb +10 -11
- data/benchmark/nearest_segment.rb +10 -6
- data/benchmark/objectspace_memsize.rb +14 -1
- data/benchmark/parse_throughput.rb +13 -10
- data/benchmark/rss_stability.rb +35 -24
- data/docs/BENCHMARKING.md +28 -1
- data/docs/CONCURRENCY.md +8 -0
- data/docs/GEOMETRY_QUERIES.md +42 -0
- data/docs/LIMITATIONS.md +12 -0
- data/ext/tg_geometry/tg_geometry_ext.c +893 -18
- data/lib/tg/geometry/version.rb +1 -1
- data/lib/tg/geometry.rb +40 -0
- data/spec/distance_spec.rb +258 -0
- metadata +11 -6
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "_support"
|
|
4
|
+
|
|
5
|
+
TGGeometryBench.say_header("distance_point_geom")
|
|
6
|
+
|
|
7
|
+
VERTEX_COUNTS = (TGGeometryBench.full? ? [16, 128, 1_024, 8_192] : [16, 128, 1_024]).freeze
|
|
8
|
+
|
|
9
|
+
def regular_polygon(vertex_count, radius: 1.0)
|
|
10
|
+
points = vertex_count.times.map do |i|
|
|
11
|
+
angle = (2.0 * Math::PI * i) / vertex_count
|
|
12
|
+
[Math.cos(angle) * radius, Math.sin(angle) * radius]
|
|
13
|
+
end
|
|
14
|
+
points << points.first
|
|
15
|
+
TG::Geometry.polygon(points)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def zigzag_line(vertex_count, scale: 1.0)
|
|
19
|
+
points = vertex_count.times.map { |i| [i.to_f * scale, (i.even? ? 0.0 : 1.0) * scale] }
|
|
20
|
+
TG::Geometry.line_string(points)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
VERTEX_COUNTS.each do |vertices|
|
|
24
|
+
geometries = {
|
|
25
|
+
polygon_xy: regular_polygon(vertices, radius: 10.0),
|
|
26
|
+
line_xy: zigzag_line(vertices),
|
|
27
|
+
polygon_lnglat_meters: regular_polygon(vertices, radius: 0.01),
|
|
28
|
+
line_lnglat_meters: zigzag_line(vertices, scale: 0.0001)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
geometries.each do |kind, geom|
|
|
32
|
+
method, args = case kind
|
|
33
|
+
when :polygon_xy then [:distance_to_xy, [20.0, 0.0]]
|
|
34
|
+
when :line_xy then [:distance_to_xy, [vertices / 2.0, 5.0]]
|
|
35
|
+
when :polygon_lnglat_meters then [:distance_to_lnglat_meters, [0.02, 0.0]]
|
|
36
|
+
when :line_lnglat_meters then [:distance_to_lnglat_meters, [vertices / 20_000.0, 0.002]]
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
stats = TGGeometryBench.measure_counted(initial_iterations: TGGeometryBench.initial_iterations(1_000)) do |iter|
|
|
40
|
+
iter.times { geom.public_send(method, *args) }
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
TGGeometryBench.report(
|
|
44
|
+
"distance_point_geom",
|
|
45
|
+
{ kind: kind, n: vertices, method: method, note: "distance methods keep the GVL" },
|
|
46
|
+
stats: stats
|
|
47
|
+
)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "_support"
|
|
4
|
+
|
|
5
|
+
TGGeometryBench.say_header("distance_within_radius")
|
|
6
|
+
|
|
7
|
+
COUNTS = (TGGeometryBench.full? ? [1_000, 10_000, 50_000] : [1_000, 10_000]).freeze
|
|
8
|
+
# Contract-required honesty case: tiny index + radius covering the whole data extent.
|
|
9
|
+
# This shows the crossover zone where paying for the rtree prefilter may not help
|
|
10
|
+
# compared to a direct full scan. The result is empirical: on some Ruby/CPU builds
|
|
11
|
+
# rtree may still win, but the benchmark must expose the scenario instead of
|
|
12
|
+
# only showing selective-radius wins.
|
|
13
|
+
TINY_COUNTS = (TGGeometryBench.full? ? [50, 100, 250] : [50, 100]).freeze
|
|
14
|
+
|
|
15
|
+
def box(x, y, size = 0.8)
|
|
16
|
+
TG::Geometry.polygon([[x, y], [x + size, y], [x + size, y + size], [x, y + size], [x, y]])
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def entries(count)
|
|
20
|
+
count.times.map do |i|
|
|
21
|
+
x = (i % 250).to_f * 2.0
|
|
22
|
+
y = (i / 250).to_f * 2.0
|
|
23
|
+
[i, box(x, y)]
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def brute_force(entries, x, y, radius)
|
|
28
|
+
entries.filter_map do |id, geom|
|
|
29
|
+
distance = geom.distance_to_xy(x, y)
|
|
30
|
+
[id, distance] if distance <= radius
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def measure_radius_case(data, index, count, query_name, x, y, radius, initial:)
|
|
35
|
+
[
|
|
36
|
+
[:rtree_prefilter_exact_filter, -> { index.within_distance_xy(x, y, radius) }],
|
|
37
|
+
[:brute_force_full_scan, -> { brute_force(data, x, y, radius) }]
|
|
38
|
+
].each do |method, callable|
|
|
39
|
+
stats = TGGeometryBench.measure_counted(initial_iterations: TGGeometryBench.initial_iterations(initial)) do |iterations|
|
|
40
|
+
iterations.times { callable.call }
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
TGGeometryBench.report(
|
|
44
|
+
"distance_within_radius",
|
|
45
|
+
{ n: count, query: query_name, method: method, note: "prefilter vs full scan; distance radius search keeps the GVL" },
|
|
46
|
+
stats: stats
|
|
47
|
+
)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
COUNTS.each do |count|
|
|
52
|
+
data = entries(count)
|
|
53
|
+
index = TG::Geometry::Index.build(data, via: :geom, strategy: :rtree)
|
|
54
|
+
|
|
55
|
+
{
|
|
56
|
+
selective: [10.5, 10.5, 1.0, 250],
|
|
57
|
+
broad: [150.0, 20.0, 180.0, 25]
|
|
58
|
+
}.each do |query_name, (x, y, radius, initial)|
|
|
59
|
+
measure_radius_case(data, index, count, query_name, x, y, radius, initial: initial)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
TINY_COUNTS.each do |count|
|
|
64
|
+
data = entries(count)
|
|
65
|
+
index = TG::Geometry::Index.build(data, via: :geom, strategy: :rtree)
|
|
66
|
+
|
|
67
|
+
# Query at the tiny grid center with a radius that covers the whole generated extent.
|
|
68
|
+
max_x = ((count - 1) % 250).to_f * 2.0 + 0.8
|
|
69
|
+
max_y = ((count - 1) / 250).to_f * 2.0 + 0.8
|
|
70
|
+
x = max_x / 2.0
|
|
71
|
+
y = max_y / 2.0
|
|
72
|
+
radius = Math.hypot(max_x, max_y) + 1.0
|
|
73
|
+
|
|
74
|
+
measure_radius_case(data, index, count, :tiny_full_extent, x, y, radius, initial: 100)
|
|
75
|
+
end
|
data/benchmark/ewkb_roundtrip.rb
CHANGED
|
@@ -1,28 +1,37 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "benchmark"
|
|
4
3
|
require_relative "_support"
|
|
5
4
|
|
|
5
|
+
TGGeometryBench.say_header("ewkb_roundtrip")
|
|
6
|
+
|
|
6
7
|
geom = TG::Geometry.polygon([[0, 0], [10, 0], [10, 10], [0, 10], [0, 0]], srid: 4326)
|
|
7
8
|
ewkb = geom.to_ewkb
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
100_000.times { TG::Geometry.parse_wkb(ewkb).to_ewkb }
|
|
12
|
-
end
|
|
10
|
+
stats = TGGeometryBench.measure_counted(initial_iterations: TGGeometryBench.initial_iterations(5_000)) do |iterations|
|
|
11
|
+
iterations.times { TG::Geometry.parse_wkb(ewkb).to_ewkb }
|
|
13
12
|
end
|
|
14
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
|
+
|
|
15
20
|
if ENV["WITH_RGEO"]
|
|
16
21
|
begin
|
|
17
22
|
require "rgeo"
|
|
18
23
|
factory = RGeo::Cartesian.factory(srid: 4326)
|
|
19
24
|
rgeo_geom = factory.parse_wkt("POLYGON ((0 0, 10 0, 10 10, 0 10, 0 0))")
|
|
20
25
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
100_000.times { factory.parse_wkt(rgeo_geom.as_text) }
|
|
24
|
-
end
|
|
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) }
|
|
25
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
|
+
)
|
|
26
35
|
rescue LoadError
|
|
27
36
|
warn "rgeo is not installed"
|
|
28
37
|
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
|
data/benchmark/geom_query.rb
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "benchmark"
|
|
4
3
|
require_relative "_support"
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
STRATEGIES = %i[flat rtree].freeze
|
|
5
|
+
TGGeometryBench.say_header("geom_query")
|
|
8
6
|
|
|
7
|
+
COUNTS = (TGGeometryBench.full? ? [1_000, 10_000, 50_000] : [1_000, 10_000]).freeze
|
|
8
|
+
STRATEGIES = %i[flat rtree].freeze
|
|
9
9
|
|
|
10
10
|
def box(x, y, size = 1.0)
|
|
11
11
|
TG::Geometry.polygon([[x, y], [x + size, y], [x + size, y + size], [x, y + size], [x, y]])
|
|
@@ -18,16 +18,26 @@ COUNTS.each do |count|
|
|
|
18
18
|
[i, box(x, y)]
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
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
|
+
}
|
|
23
26
|
|
|
24
|
-
puts "\n#{count} polygons"
|
|
25
27
|
STRATEGIES.each do |strategy|
|
|
26
28
|
index = TG::Geometry::Index.build(entries, via: :geom, strategy: strategy)
|
|
27
29
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
+
)
|
|
31
41
|
end
|
|
32
42
|
end
|
|
33
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
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "benchmark"
|
|
4
3
|
require_relative "_support"
|
|
5
4
|
|
|
5
|
+
TGGeometryBench.say_header("nearest_segment")
|
|
6
|
+
|
|
6
7
|
[100, 1_000, 10_000].each do |count|
|
|
7
8
|
points = count.times.map do |i|
|
|
8
9
|
angle = 2.0 * Math::PI * i / count
|
|
@@ -11,10 +12,13 @@ require_relative "_support"
|
|
|
11
12
|
points << points.first
|
|
12
13
|
ring = TG::Geometry.polygon(points).polygon.exterior_ring
|
|
13
14
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
x.report("1M nearest_segment calls") do
|
|
17
|
-
1_000_000.times { ring.nearest_segment(0.25, 0.33) }
|
|
18
|
-
end
|
|
15
|
+
stats = TGGeometryBench.measure_counted(initial_iterations: TGGeometryBench.initial_iterations(10_000)) do |iterations|
|
|
16
|
+
iterations.times { ring.nearest_segment(0.25, 0.33) }
|
|
19
17
|
end
|
|
18
|
+
|
|
19
|
+
TGGeometryBench.report(
|
|
20
|
+
"nearest_segment",
|
|
21
|
+
{ segments: ring.num_segments, query: "0.25:0.33" },
|
|
22
|
+
stats: stats
|
|
23
|
+
)
|
|
20
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
|