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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7cb54eb85cd48036c2f32eb5f95fa3d19cbf994d375bc102b05f2d45dfb1f8ec
|
|
4
|
+
data.tar.gz: c5ff1c066708d8826c125185f34a346e1f1e2bf66f65fb1b1eb13e75bf955aad
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 981e37eb58fac8eaa20c3fa9604b751c263330e3f3dc2e8614cb1c91e36eb76d31431b4a13b3cbcd44475323d5ca76ec3e58c013d8c16de9d28e2ec890704962
|
|
7
|
+
data.tar.gz: 05db4ca563cd1c22e13d5c27e440845452397f50516cf01dde8675032fb3e12a527ba0fe52ccd32bc1fe313b3f8d083f46c17a54e4e8c316b6e8ce9766c278d5
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,35 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.2 - unreleased
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Added explicit point-to-geometry distance APIs on `TG::Geometry::Geom`:
|
|
8
|
+
`distance_to_lnglat_meters`, `boundary_distance_to_lnglat_meters`,
|
|
9
|
+
`nearest_point_lnglat`, `distance_to_xy`, `boundary_distance_to_xy`, and
|
|
10
|
+
`nearest_point_xy`.
|
|
11
|
+
- Added `TG::Geometry::Index#within_distance_lnglat_meters`,
|
|
12
|
+
`#within_distance_ids_lnglat_meters`, `#within_distance_xy`, and
|
|
13
|
+
`#within_distance_ids_xy` using bbox prefilter plus exact distance filtering.
|
|
14
|
+
- Added distance specs and benchmarks for point-to-geometry and radius queries, including
|
|
15
|
+
tiny-index/full-extent radius cases and distance receiver memory-accounting checks.
|
|
16
|
+
|
|
17
|
+
### Clarified
|
|
18
|
+
|
|
19
|
+
- lng/lat distance is approximate local equirectangular meters for local geofencing,
|
|
20
|
+
not geodesic distance; GeoJSON segments remain straight coordinate segments.
|
|
21
|
+
- The lng/lat metric is raw planar and does not wrap or split at the antimeridian.
|
|
22
|
+
- Distance query methods keep the GVL and do not claim Falcon/Async/Ractor behavior.
|
|
23
|
+
|
|
24
|
+
## 0.3.1 - 28.05.2026
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
- Optimized R-tree-backed point lookup by removing per-query candidate bitmap
|
|
28
|
+
allocation and full entry scan from Index#find_covering. The new path tracks
|
|
29
|
+
the lowest matching ordinal directly in the R-tree callback, preserving
|
|
30
|
+
insertion-order semantics while significantly improving rtree point queries
|
|
31
|
+
and packed batch point lookup.
|
|
32
|
+
|
|
3
33
|
## 0.3.0 - 27.05.2026
|
|
4
34
|
|
|
5
35
|
### Added
|
data/README.md
CHANGED
|
@@ -188,6 +188,46 @@ Predicate direction is explicit:
|
|
|
188
188
|
|
|
189
189
|
Results are ids only and preserve insertion order. Duplicate ids remain possible if duplicate ids were inserted.
|
|
190
190
|
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
## Point-to-geometry distance
|
|
194
|
+
|
|
195
|
+
`TG::Geometry::Geom` exposes explicit point distance APIs. Units are encoded in the method names; there is no `metric:` option and no automatic lng/lat-vs-XY detection.
|
|
196
|
+
|
|
197
|
+
```ruby
|
|
198
|
+
zone.distance_to_lnglat_meters(lng, lat) # => Float, approximate meters
|
|
199
|
+
zone.boundary_distance_to_lnglat_meters(lng, lat) # => Float, approximate meters
|
|
200
|
+
zone.nearest_point_lnglat(lng, lat) # => [lng, lat]
|
|
201
|
+
|
|
202
|
+
zone.distance_to_xy(x, y) # => Float in input coordinate units
|
|
203
|
+
zone.boundary_distance_to_xy(x, y) # => Float in input coordinate units
|
|
204
|
+
zone.nearest_point_xy(x, y) # => [x, y]
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
For areal geometries (`Polygon`, `MultiPolygon`, and areal collection members), `distance_to_*` returns `0.0` for points inside the covered area or on the boundary. Holes are excluded, so a point inside a hole measures to the nearest hole ring. `boundary_distance_to_*` always measures to the nearest boundary/ring/segment; for an interior point it does not return `0.0` merely because the point is covered. `nearest_point_*` returns the nearest boundary point for areal geometries, including interior queries.
|
|
208
|
+
|
|
209
|
+
Distance methods for lng/lat geometries return approximate meters using a per-query local equirectangular frame. Segments are GeoJSON straight coordinate segments, not great-circle arcs. This is geofencing-grade metric distance, not geodesy. Accuracy is intended for local geofencing and degrades with latitude separation.
|
|
210
|
+
|
|
211
|
+
The lng/lat metric is raw planar lng/lat and does not wrap longitude at `+/-180`. A point at `179.9` and a point at `-179.9` are treated as about `360` degrees apart, matching the gem's planar `covers_xy?` model. Data that crosses the antimeridian should be cut at `+/-180` before import.
|
|
212
|
+
|
|
213
|
+
`Index` supports radius filters with an rtree bbox prefilter followed by exact distance filtering:
|
|
214
|
+
|
|
215
|
+
```ruby
|
|
216
|
+
index.within_distance_lnglat_meters(lng, lat, radius_m, sort: false)
|
|
217
|
+
# => [[id, distance_m], ...]
|
|
218
|
+
index.within_distance_ids_lnglat_meters(lng, lat, radius_m)
|
|
219
|
+
# => [id, ...]
|
|
220
|
+
|
|
221
|
+
index.within_distance_xy(x, y, radius, sort: false)
|
|
222
|
+
# => [[id, distance], ...]
|
|
223
|
+
index.within_distance_ids_xy(x, y, radius)
|
|
224
|
+
# => [id, ...]
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
`sort: true` sorts filtered `[id, distance]` pairs by ascending distance. The ids variants intentionally do not accept `sort:`. Index kNN / `nearest_ids` is not implemented.
|
|
228
|
+
|
|
229
|
+
Distance radius benchmarks compare `rtree prefilter + exact filter` against a brute-force full index scan. Any ratio from those benchmarks is a prefilter-vs-full-scan result, not a claim that the exact distance calculation itself is hundreds of times faster. The benchmark suite includes tiny-index/full-extent cases where the rtree prefilter may be neutral or slower.
|
|
230
|
+
|
|
191
231
|
## GeoJSON FeatureSource
|
|
192
232
|
|
|
193
233
|
`TG::Geometry::FeatureSource` reads GeoJSON `FeatureCollection` sources without `JSON.parse` of the whole document into Ruby Hash/Array objects.
|
|
@@ -345,7 +385,7 @@ Not included:
|
|
|
345
385
|
- Z/M variants of array constructors;
|
|
346
386
|
- Public `release_gvl:` option.
|
|
347
387
|
|
|
348
|
-
TG works in planar XY coordinates. If lon/lat coordinates are passed in, length, area, perimeter, and nearest-segment distances are in input coordinate units, not meters.
|
|
388
|
+
TG works in planar XY coordinates. If lon/lat coordinates are passed in, length, area, perimeter, and low-level nearest-segment distances are in input coordinate units, not meters. The explicit `*_lnglat_meters` point-to-geometry APIs are the exception: they return approximate local meters using the query-local frame documented above, not geodesic meters.
|
|
349
389
|
|
|
350
390
|
## Development
|
|
351
391
|
|
data/benchmark/_support.rb
CHANGED
|
@@ -21,14 +21,454 @@ module TGGeometryBench
|
|
|
21
21
|
module_function
|
|
22
22
|
|
|
23
23
|
SIZES = [100, 500, 1_000, 5_000, 50_000].freeze
|
|
24
|
-
FAST_SIZES = [100,
|
|
24
|
+
FAST_SIZES = [100, 1_000, 5_000].freeze
|
|
25
|
+
|
|
26
|
+
DEFAULT_REPEATS = 5
|
|
27
|
+
DEFAULT_MIN_SECONDS = 0.25
|
|
28
|
+
DEFAULT_WARMUP_SECONDS = 0.05
|
|
29
|
+
DEFAULT_MAX_ITERATIONS = 100_000_000
|
|
30
|
+
|
|
31
|
+
def full?
|
|
32
|
+
ENV["TGEOMETRY_BENCH_FULL"] == "1"
|
|
33
|
+
end
|
|
25
34
|
|
|
26
35
|
def sizes
|
|
27
|
-
|
|
36
|
+
full? ? SIZES : FAST_SIZES
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def env_integer(name, default, min: nil)
|
|
40
|
+
value = Integer(ENV.fetch(name, default.to_s))
|
|
41
|
+
if min && value < min
|
|
42
|
+
raise ArgumentError, "#{name} must be >= #{min}, got #{value}"
|
|
43
|
+
end
|
|
44
|
+
value
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def env_float(name, default, min: nil)
|
|
48
|
+
value = Float(ENV.fetch(name, default.to_s))
|
|
49
|
+
if min && value < min
|
|
50
|
+
raise ArgumentError, "#{name} must be >= #{min}, got #{value}"
|
|
51
|
+
end
|
|
52
|
+
value
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def initial_iterations(default)
|
|
56
|
+
env_integer("TGEOMETRY_BENCH_ITERATIONS", default, min: 1)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def repeats(default = DEFAULT_REPEATS)
|
|
60
|
+
env_integer("TGEOMETRY_BENCH_REPEATS", default, min: 1)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def min_seconds(default = DEFAULT_MIN_SECONDS)
|
|
64
|
+
env_float("TGEOMETRY_BENCH_MIN_SECONDS", default, min: 0.0)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def warmup_seconds(default = DEFAULT_WARMUP_SECONDS)
|
|
68
|
+
env_float("TGEOMETRY_BENCH_WARMUP_SECONDS", default, min: 0.0)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def max_iterations(default = DEFAULT_MAX_ITERATIONS)
|
|
72
|
+
env_integer("TGEOMETRY_BENCH_MAX_ITERATIONS", default, min: 1)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def adaptive?
|
|
76
|
+
ENV.fetch("TGEOMETRY_BENCH_ADAPTIVE", "1") != "0"
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def disable_gc_during_timed?
|
|
80
|
+
ENV["TGEOMETRY_BENCH_DISABLE_GC"] == "1"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def monotonic
|
|
84
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def median(values)
|
|
88
|
+
sorted = values.sort
|
|
89
|
+
n = sorted.length
|
|
90
|
+
return 0.0 if n.zero?
|
|
91
|
+
|
|
92
|
+
mid = n / 2
|
|
93
|
+
n.odd? ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2.0
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def percentile(values, pct)
|
|
97
|
+
sorted = values.sort
|
|
98
|
+
return 0.0 if sorted.empty?
|
|
99
|
+
|
|
100
|
+
rank = (pct * (sorted.length - 1)).round
|
|
101
|
+
sorted.fetch(rank)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def gc_start
|
|
105
|
+
GC.start(full_mark: true, immediate_sweep: true)
|
|
106
|
+
rescue ArgumentError
|
|
107
|
+
GC.start
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def timed_gc_delta
|
|
111
|
+
before = GC.stat
|
|
112
|
+
seconds = nil
|
|
113
|
+
|
|
114
|
+
if disable_gc_during_timed?
|
|
115
|
+
was_enabled = GC.enable
|
|
116
|
+
GC.disable
|
|
117
|
+
seconds = Benchmark.realtime { yield }
|
|
118
|
+
GC.enable if was_enabled
|
|
119
|
+
else
|
|
120
|
+
seconds = Benchmark.realtime { yield }
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
after = GC.stat
|
|
124
|
+
[
|
|
125
|
+
seconds,
|
|
126
|
+
after[:total_allocated_objects] - before[:total_allocated_objects],
|
|
127
|
+
after[:minor_gc_count] - before[:minor_gc_count],
|
|
128
|
+
after[:major_gc_count] - before[:major_gc_count]
|
|
129
|
+
]
|
|
130
|
+
ensure
|
|
131
|
+
GC.enable if disable_gc_during_timed?
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def calibrate_iterations(initial_iterations:, min_seconds:, max_iterations: self.max_iterations, &block)
|
|
135
|
+
iterations = initial_iterations
|
|
136
|
+
return iterations unless adaptive?
|
|
137
|
+
return iterations if min_seconds <= 0.0
|
|
138
|
+
|
|
139
|
+
loop do
|
|
140
|
+
gc_start
|
|
141
|
+
seconds = Benchmark.realtime { block.call(iterations) }
|
|
142
|
+
break iterations if seconds >= min_seconds || iterations >= max_iterations
|
|
143
|
+
|
|
144
|
+
if seconds <= 0.0
|
|
145
|
+
iterations = [iterations * 10, max_iterations].min
|
|
146
|
+
next
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
target = (iterations * (min_seconds / seconds) * 1.50).ceil
|
|
150
|
+
iterations = [[target, iterations * 2].max, max_iterations].min
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def warmup_for(seconds, iterations, &block)
|
|
155
|
+
return if seconds <= 0.0
|
|
156
|
+
|
|
157
|
+
deadline = monotonic + seconds
|
|
158
|
+
begin
|
|
159
|
+
block.call(iterations)
|
|
160
|
+
end while monotonic < deadline
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def measure_counted(initial_iterations:, operations_per_iteration: 1, min_seconds: self.min_seconds,
|
|
164
|
+
repeats: self.repeats, warmup_seconds: self.warmup_seconds,
|
|
165
|
+
max_iterations: self.max_iterations, &block)
|
|
166
|
+
iterations = calibrate_iterations(
|
|
167
|
+
initial_iterations: initial_iterations,
|
|
168
|
+
min_seconds: min_seconds,
|
|
169
|
+
max_iterations: max_iterations,
|
|
170
|
+
&block
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
warmup_for(warmup_seconds, iterations, &block)
|
|
174
|
+
|
|
175
|
+
seconds_samples = []
|
|
176
|
+
allocation_samples = []
|
|
177
|
+
minor_gc_samples = []
|
|
178
|
+
major_gc_samples = []
|
|
179
|
+
|
|
180
|
+
repeats.times do
|
|
181
|
+
gc_start
|
|
182
|
+
seconds, allocations, minor_gc, major_gc = timed_gc_delta { block.call(iterations) }
|
|
183
|
+
seconds_samples << seconds
|
|
184
|
+
allocation_samples << allocations
|
|
185
|
+
minor_gc_samples << minor_gc
|
|
186
|
+
major_gc_samples << major_gc
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
total_operations = iterations * operations_per_iteration
|
|
190
|
+
median_sec = median(seconds_samples)
|
|
191
|
+
|
|
192
|
+
{
|
|
193
|
+
iterations: iterations,
|
|
194
|
+
operations_per_iteration: operations_per_iteration,
|
|
195
|
+
operations_per_repeat: total_operations,
|
|
196
|
+
repeats: repeats,
|
|
197
|
+
min_seconds: min_seconds,
|
|
198
|
+
warmup_seconds: warmup_seconds,
|
|
199
|
+
best_sec: seconds_samples.min,
|
|
200
|
+
median_sec: median_sec,
|
|
201
|
+
worst_sec: seconds_samples.max,
|
|
202
|
+
mean_sec: seconds_samples.sum / seconds_samples.length,
|
|
203
|
+
p90_sec: percentile(seconds_samples, 0.90),
|
|
204
|
+
ops_per_sec: median_sec.positive? ? total_operations / median_sec : 0.0,
|
|
205
|
+
ns_per_op: median_sec.positive? ? (median_sec / total_operations) * 1e9 : 0.0,
|
|
206
|
+
spread_pct: seconds_samples.min.positive? ? ((seconds_samples.max - seconds_samples.min) / seconds_samples.min) * 100.0 : 0.0,
|
|
207
|
+
median_allocations: median(allocation_samples),
|
|
208
|
+
allocations_per_op: total_operations.positive? ? median(allocation_samples) / total_operations.to_f : 0.0,
|
|
209
|
+
median_minor_gc: median(minor_gc_samples),
|
|
210
|
+
median_major_gc: median(major_gc_samples),
|
|
211
|
+
samples_sec: seconds_samples
|
|
212
|
+
}
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def format_value(value)
|
|
216
|
+
case value
|
|
217
|
+
when Float
|
|
218
|
+
if value.finite?
|
|
219
|
+
"%.6f" % value
|
|
220
|
+
else
|
|
221
|
+
value.to_s
|
|
222
|
+
end
|
|
223
|
+
when Array
|
|
224
|
+
value.join(":")
|
|
225
|
+
else
|
|
226
|
+
value.to_s.gsub(/\s+/, "_")
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def output_format
|
|
231
|
+
ENV.fetch("TGEOMETRY_BENCH_FORMAT", "table")
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def kv_output?
|
|
235
|
+
%w[kv both].include?(output_format)
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def table_output?
|
|
239
|
+
%w[table both].include?(output_format)
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def report_rows
|
|
243
|
+
@report_rows ||= []
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def kv_line(payload)
|
|
247
|
+
payload.map { |key, value| "#{key}=#{format_value(value)}" }.join(" ")
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def report(benchmark, fields = nil, stats: nil, **kwargs)
|
|
251
|
+
fields = (fields || {}).merge(kwargs)
|
|
252
|
+
|
|
253
|
+
payload = {
|
|
254
|
+
benchmark: benchmark,
|
|
255
|
+
ruby: RUBY_VERSION,
|
|
256
|
+
platform: RUBY_PLATFORM,
|
|
257
|
+
full: full?,
|
|
258
|
+
adaptive: adaptive?,
|
|
259
|
+
gc_disabled: disable_gc_during_timed?
|
|
260
|
+
}.merge(fields)
|
|
261
|
+
|
|
262
|
+
if stats
|
|
263
|
+
payload = payload.merge(
|
|
264
|
+
iterations: stats[:iterations],
|
|
265
|
+
ops_per_iteration: stats[:operations_per_iteration],
|
|
266
|
+
operations: stats[:operations_per_repeat],
|
|
267
|
+
repeats: stats[:repeats],
|
|
268
|
+
min_seconds: stats[:min_seconds],
|
|
269
|
+
median_sec: stats[:median_sec],
|
|
270
|
+
best_sec: stats[:best_sec],
|
|
271
|
+
worst_sec: stats[:worst_sec],
|
|
272
|
+
ops_per_sec: stats[:ops_per_sec],
|
|
273
|
+
ns_per_op: stats[:ns_per_op],
|
|
274
|
+
spread_pct: stats[:spread_pct],
|
|
275
|
+
median_allocations: stats[:median_allocations],
|
|
276
|
+
allocations_per_op: stats[:allocations_per_op],
|
|
277
|
+
median_minor_gc: stats[:median_minor_gc],
|
|
278
|
+
median_major_gc: stats[:median_major_gc]
|
|
279
|
+
)
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
puts kv_line(payload) if kv_output?
|
|
283
|
+
report_rows << payload if table_output?
|
|
284
|
+
payload
|
|
28
285
|
end
|
|
29
286
|
|
|
30
|
-
|
|
31
|
-
|
|
287
|
+
TABLE_CONTEXT_COLUMNS = [
|
|
288
|
+
:kind, :n, :strategy, :mode,
|
|
289
|
+
:query, :method, :case, :format,
|
|
290
|
+
:library, :operation,
|
|
291
|
+
:point_index, :lon, :lat,
|
|
292
|
+
:rect_index, :rect,
|
|
293
|
+
:segments, :target_bytes, :payload_bytes,
|
|
294
|
+
:points_per_batch, :threads,
|
|
295
|
+
:entries, :rebuilds, :cycle, :receiver
|
|
296
|
+
].freeze
|
|
297
|
+
|
|
298
|
+
TABLE_METRIC_COLUMNS = [
|
|
299
|
+
:batches_per_sec, :ops_per_sec, :qps, :ns_per_op,
|
|
300
|
+
:median_sec, :spread_pct, :allocations_per_op,
|
|
301
|
+
:median_minor_gc, :median_major_gc,
|
|
302
|
+
:iterations, :operations,
|
|
303
|
+
:geom_memsize, :flat_memsize, :rtree_memsize, :rtree_over_flat,
|
|
304
|
+
:start_rss_kb, :peak_rss_kb, :finish_rss_kb, :drift_kb, :max_drift_kb,
|
|
305
|
+
:elapsed_sec, :queries, :sample_count, :rss_kb,
|
|
306
|
+
:before_memsize, :after_memsize, :delta_memsize, :allocated_objects
|
|
307
|
+
].freeze
|
|
308
|
+
|
|
309
|
+
TABLE_INTERNAL_COLUMNS = [
|
|
310
|
+
:benchmark, :ruby, :platform, :full, :adaptive, :gc_disabled,
|
|
311
|
+
:repeats, :min_seconds, :warmup_seconds, :max_iterations,
|
|
312
|
+
:ops_per_iteration, :median_allocations,
|
|
313
|
+
:best_sec, :worst_sec
|
|
314
|
+
].freeze
|
|
315
|
+
|
|
316
|
+
TABLE_HEADERS = {
|
|
317
|
+
kind: "kind",
|
|
318
|
+
n: "n",
|
|
319
|
+
strategy: "strategy",
|
|
320
|
+
mode: "mode",
|
|
321
|
+
query: "query",
|
|
322
|
+
method: "method",
|
|
323
|
+
case: "case",
|
|
324
|
+
format: "format",
|
|
325
|
+
library: "lib",
|
|
326
|
+
operation: "op",
|
|
327
|
+
point_index: "pt#",
|
|
328
|
+
rect_index: "rect#",
|
|
329
|
+
lon: "lon",
|
|
330
|
+
lat: "lat",
|
|
331
|
+
rect: "rect",
|
|
332
|
+
segments: "segments",
|
|
333
|
+
target_bytes: "target B",
|
|
334
|
+
payload_bytes: "payload B",
|
|
335
|
+
points_per_batch: "points/batch",
|
|
336
|
+
batches_per_sec: "batches/s",
|
|
337
|
+
ops_per_sec: "ops/s",
|
|
338
|
+
qps: "qps",
|
|
339
|
+
ns_per_op: "ns/op",
|
|
340
|
+
median_sec: "median s",
|
|
341
|
+
spread_pct: "spread %",
|
|
342
|
+
allocations_per_op: "alloc/op",
|
|
343
|
+
median_minor_gc: "minor GC",
|
|
344
|
+
median_major_gc: "major GC",
|
|
345
|
+
iterations: "iters",
|
|
346
|
+
operations: "ops",
|
|
347
|
+
threads: "threads",
|
|
348
|
+
entries: "entries",
|
|
349
|
+
rebuilds: "rebuilds",
|
|
350
|
+
queries: "queries",
|
|
351
|
+
cycle: "cycle",
|
|
352
|
+
receiver: "receiver",
|
|
353
|
+
rss_kb: "rss KB",
|
|
354
|
+
geom_memsize: "geom B",
|
|
355
|
+
flat_memsize: "flat B",
|
|
356
|
+
rtree_memsize: "rtree B",
|
|
357
|
+
rtree_over_flat: "rtree-flat B",
|
|
358
|
+
start_rss_kb: "start KB",
|
|
359
|
+
peak_rss_kb: "peak KB",
|
|
360
|
+
finish_rss_kb: "finish KB",
|
|
361
|
+
drift_kb: "drift KB",
|
|
362
|
+
max_drift_kb: "max drift KB",
|
|
363
|
+
elapsed_sec: "elapsed s",
|
|
364
|
+
sample_count: "samples",
|
|
365
|
+
before_memsize: "before B",
|
|
366
|
+
after_memsize: "after B",
|
|
367
|
+
delta_memsize: "delta B",
|
|
368
|
+
allocated_objects: "alloc objs"
|
|
369
|
+
}.freeze
|
|
370
|
+
|
|
371
|
+
def human_int(value)
|
|
372
|
+
Integer(value).to_s.reverse.gsub(/(\d{3})(?=\d)/, '\\1_').reverse
|
|
373
|
+
rescue StandardError
|
|
374
|
+
value.to_s
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
def human_rate(value)
|
|
378
|
+
v = Float(value)
|
|
379
|
+
return "0" if v.zero?
|
|
380
|
+
|
|
381
|
+
abs = v.abs
|
|
382
|
+
if abs >= 1_000_000
|
|
383
|
+
"%.2fM" % (v / 1_000_000.0)
|
|
384
|
+
elsif abs >= 1_000
|
|
385
|
+
"%.1fk" % (v / 1_000.0)
|
|
386
|
+
elsif abs >= 100
|
|
387
|
+
"%.1f" % v
|
|
388
|
+
else
|
|
389
|
+
"%.3f" % v
|
|
390
|
+
end
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
def format_table_cell(key, value)
|
|
394
|
+
return "" if value.nil?
|
|
395
|
+
|
|
396
|
+
case key
|
|
397
|
+
when :ops_per_sec, :batches_per_sec, :qps
|
|
398
|
+
human_rate(value)
|
|
399
|
+
when :ns_per_op
|
|
400
|
+
"%.1f" % Float(value)
|
|
401
|
+
when :median_sec, :elapsed_sec
|
|
402
|
+
"%.4f" % Float(value)
|
|
403
|
+
when :spread_pct
|
|
404
|
+
"%.2f" % Float(value)
|
|
405
|
+
when :allocations_per_op
|
|
406
|
+
v = Float(value)
|
|
407
|
+
v < 0.01 ? ("%.5f" % v) : ("%.3f" % v)
|
|
408
|
+
when :lon, :lat
|
|
409
|
+
"%.3f" % Float(value)
|
|
410
|
+
when :rect
|
|
411
|
+
Array(value).map { |v| (Float(v) % 1).zero? ? Integer(v).to_s : ("%.3f" % Float(v)) }.join(":")
|
|
412
|
+
when :n, :iterations, :operations, :entries, :rebuilds, :queries, :cycle,
|
|
413
|
+
:rss_kb, :geom_memsize, :flat_memsize, :rtree_memsize, :rtree_over_flat,
|
|
414
|
+
:start_rss_kb, :peak_rss_kb, :finish_rss_kb, :drift_kb, :max_drift_kb,
|
|
415
|
+
:target_bytes, :payload_bytes, :segments, :points_per_batch, :threads,
|
|
416
|
+
:sample_count, :median_minor_gc, :median_major_gc,
|
|
417
|
+
:before_memsize, :after_memsize, :delta_memsize, :allocated_objects
|
|
418
|
+
human_int(value)
|
|
419
|
+
when :full, :adaptive, :gc_disabled
|
|
420
|
+
value ? "yes" : "no"
|
|
421
|
+
else
|
|
422
|
+
value.to_s
|
|
423
|
+
end
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
def table_columns(rows)
|
|
427
|
+
keys = rows.flat_map(&:keys).uniq
|
|
428
|
+
context = TABLE_CONTEXT_COLUMNS.select { |key| keys.include?(key) }
|
|
429
|
+
metrics = TABLE_METRIC_COLUMNS.select { |key| keys.include?(key) }
|
|
430
|
+
extras = keys - context - metrics - TABLE_INTERNAL_COLUMNS
|
|
431
|
+
context + extras + metrics
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
def print_table(rows, columns)
|
|
435
|
+
headers = columns.map { |key| TABLE_HEADERS.fetch(key, key.to_s) }
|
|
436
|
+
body = rows.map do |row|
|
|
437
|
+
columns.map { |key| format_table_cell(key, row[key]) }
|
|
438
|
+
end
|
|
439
|
+
widths = headers.each_with_index.map do |header, index|
|
|
440
|
+
([header.length] + body.map { |cells| cells[index].length }).max
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
separator = "+-#{widths.map { |w| "-" * w }.join("-+-")}-+"
|
|
444
|
+
puts separator
|
|
445
|
+
puts "| #{headers.each_with_index.map { |h, i| h.ljust(widths[i]) }.join(" | ")} |"
|
|
446
|
+
puts separator
|
|
447
|
+
body.each do |cells|
|
|
448
|
+
puts "| #{cells.each_with_index.map { |cell, i| cell.rjust(widths[i]) }.join(" | ")} |"
|
|
449
|
+
end
|
|
450
|
+
puts separator
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
def flush_reports
|
|
454
|
+
rows = report_rows
|
|
455
|
+
return if rows.empty?
|
|
456
|
+
|
|
457
|
+
rows.group_by { |row| row[:benchmark] }.each do |benchmark, group_rows|
|
|
458
|
+
notes, measured = group_rows.partition { |row| row.key?(:note) && row.keys.none? { |key| TABLE_METRIC_COLUMNS.include?(key) } }
|
|
459
|
+
|
|
460
|
+
notes.each do |row|
|
|
461
|
+
puts "\n-- #{benchmark} --"
|
|
462
|
+
puts row[:note]
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
next if measured.empty?
|
|
466
|
+
|
|
467
|
+
puts "\n-- #{benchmark} --"
|
|
468
|
+
print_table(measured, table_columns(measured))
|
|
469
|
+
end
|
|
470
|
+
ensure
|
|
471
|
+
rows&.clear
|
|
32
472
|
end
|
|
33
473
|
|
|
34
474
|
def box_wkt(min_x, min_y, max_x, max_y)
|
|
@@ -91,6 +531,11 @@ module TGGeometryBench
|
|
|
91
531
|
end
|
|
92
532
|
end
|
|
93
533
|
|
|
534
|
+
def repeated_points(kind, count)
|
|
535
|
+
seed = points_for(kind)
|
|
536
|
+
Array.new(count) { |i| seed[i % seed.length] }
|
|
537
|
+
end
|
|
538
|
+
|
|
94
539
|
def packed_points(points)
|
|
95
540
|
points.flatten.pack("d*")
|
|
96
541
|
end
|
|
@@ -110,6 +555,13 @@ module TGGeometryBench
|
|
|
110
555
|
|
|
111
556
|
def say_header(title)
|
|
112
557
|
puts "\n== #{title} =="
|
|
113
|
-
puts "ruby=#{RUBY_VERSION} platform=#{RUBY_PLATFORM}
|
|
558
|
+
puts "ruby=#{RUBY_VERSION} platform=#{RUBY_PLATFORM} sizes=#{sizes.join(":")} repeats=#{repeats} " \
|
|
559
|
+
"min_seconds=#{format_table_cell(:median_sec, min_seconds)} warmup=#{format_table_cell(:median_sec, warmup_seconds)} " \
|
|
560
|
+
"adaptive=#{adaptive? ? "yes" : "no"} gc_disabled=#{disable_gc_during_timed? ? "yes" : "no"}"
|
|
561
|
+
puts "output=table; set TGEOMETRY_BENCH_FORMAT=kv for machine-readable key=value lines" if table_output? && !kv_output?
|
|
114
562
|
end
|
|
115
563
|
end
|
|
564
|
+
|
|
565
|
+
at_exit do
|
|
566
|
+
TGGeometryBench.flush_reports if TGGeometryBench.table_output?
|
|
567
|
+
end
|
|
@@ -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,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "_support"
|
|
4
|
+
require "objspace"
|
|
5
|
+
|
|
6
|
+
TGGeometryBench.say_header("distance_memory_accounting")
|
|
7
|
+
|
|
8
|
+
geom = TG::Geometry.polygon([[0, 0], [10, 0], [10, 10], [0, 10], [0, 0]])
|
|
9
|
+
index = TG::Geometry::Index.build([[:zone, geom]], via: :geom, strategy: :rtree)
|
|
10
|
+
|
|
11
|
+
CASES = [
|
|
12
|
+
["geom_distance_to_xy", "Geom", geom, -> { geom.distance_to_xy(5, 5) }],
|
|
13
|
+
["geom_boundary_distance_to_xy", "Geom", geom, -> { geom.boundary_distance_to_xy(5, 5) }],
|
|
14
|
+
["geom_nearest_point_xy", "Geom", geom, -> { geom.nearest_point_xy(5, 5) }],
|
|
15
|
+
["geom_distance_to_lnglat_meters", "Geom", geom, -> { geom.distance_to_lnglat_meters(0.01, 0.01) }],
|
|
16
|
+
["index_within_distance_xy", "Index", index, -> { index.within_distance_xy(5, 5, 10) }],
|
|
17
|
+
["index_within_distance_lnglat_meters", "Index", index, -> { index.within_distance_lnglat_meters(0.01, 0.01, 2_000) }]
|
|
18
|
+
].freeze
|
|
19
|
+
|
|
20
|
+
ITERATIONS = TGGeometryBench.env_integer("TGEOMETRY_DISTANCE_MEMORY_ITERATIONS", 10_000, min: 1)
|
|
21
|
+
|
|
22
|
+
CASES.each do |name, receiver_name, receiver, callable|
|
|
23
|
+
TGGeometryBench.gc_start
|
|
24
|
+
before_memsize = ObjectSpace.memsize_of(receiver)
|
|
25
|
+
before_allocated = GC.stat[:total_allocated_objects]
|
|
26
|
+
|
|
27
|
+
ITERATIONS.times { callable.call }
|
|
28
|
+
|
|
29
|
+
TGGeometryBench.gc_start
|
|
30
|
+
after_memsize = ObjectSpace.memsize_of(receiver)
|
|
31
|
+
after_allocated = GC.stat[:total_allocated_objects]
|
|
32
|
+
|
|
33
|
+
TGGeometryBench.report(
|
|
34
|
+
"distance_memory_accounting",
|
|
35
|
+
{
|
|
36
|
+
case: name,
|
|
37
|
+
receiver: receiver_name,
|
|
38
|
+
operations: ITERATIONS,
|
|
39
|
+
before_memsize: before_memsize,
|
|
40
|
+
after_memsize: after_memsize,
|
|
41
|
+
delta_memsize: after_memsize - before_memsize,
|
|
42
|
+
allocated_objects: after_allocated - before_allocated,
|
|
43
|
+
note: "delta B must stay 0; Ruby result allocations are expected for arrays/pairs"
|
|
44
|
+
}
|
|
45
|
+
)
|
|
46
|
+
end
|