perf: batch county-crossing detection into one query per activity
geo::crossings_between issued one DB round trip per consecutive GPS sample pair (tens of thousands per multi-hour activity), even though almost every segment stays inside a single county and produces no crossing at all. geo::crossings_for_track batches all segments of a track into a single query via UNNEST ... WITH ORDINALITY. The per-segment geometry logic (endpoint county lookup via the GiST index, boundary intersection, ST_LineLocatePoint for the fractional position) is unchanged, it now just runs once per activity instead of once per segment.
This commit is contained in:
parent
2ab16a68b7
commit
5a49624435
2 changed files with 73 additions and 46 deletions
79
src/geo.rs
79
src/geo.rs
|
|
@ -12,43 +12,62 @@ pub fn interpolate_time(a: DateTime<Utc>, b: DateTime<Utc>, fraction: f64) -> Da
|
|||
a + Duration::milliseconds((duration_ms as f64 * fraction).round() as i64)
|
||||
}
|
||||
|
||||
/// Detects all county crossings between two consecutive GPS samples.
|
||||
/// Detects all county crossings along an entire GPS track in a single
|
||||
/// database round trip.
|
||||
///
|
||||
/// The geometry itself is handled by PostGIS. Because the endpoint counties
|
||||
/// are used to constrain the query, the GiST geometry index remains useful.
|
||||
/// ST_LineLocatePoint gives the position of the intersection on the original
|
||||
/// track segment; that fraction is then used for millisecond timestamp
|
||||
/// interpolation.
|
||||
pub async fn crossings_between(
|
||||
/// This used to run once per consecutive point pair, i.e. one round trip
|
||||
/// per GPS sample (tens of thousands for a multi-hour activity), each
|
||||
/// paying full network/connection overhead for what is usually a no-op
|
||||
/// (most segments stay inside a single county). This version batches every
|
||||
/// segment of the track into one query via `UNNEST ... WITH ORDINALITY`,
|
||||
/// so the per-segment geometry logic is unchanged (endpoint county lookup
|
||||
/// via the GiST index, boundary intersection, `ST_LineLocatePoint` for the
|
||||
/// fractional position) but it now runs once per activity instead of once
|
||||
/// per segment.
|
||||
pub async fn crossings_for_track(
|
||||
db: &PgPool,
|
||||
a: &TrackPoint,
|
||||
b: &TrackPoint,
|
||||
points: &[TrackPoint],
|
||||
) -> Result<Vec<DetectedCrossing>> {
|
||||
if points.len() < 2 {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let segment_count = points.len() - 1;
|
||||
let mut lon1 = Vec::with_capacity(segment_count);
|
||||
let mut lat1 = Vec::with_capacity(segment_count);
|
||||
let mut lon2 = Vec::with_capacity(segment_count);
|
||||
let mut lat2 = Vec::with_capacity(segment_count);
|
||||
|
||||
for pair in points.windows(2) {
|
||||
lon1.push(pair[0].lon);
|
||||
lat1.push(pair[0].lat);
|
||||
lon2.push(pair[1].lon);
|
||||
lat2.push(pair[1].lat);
|
||||
}
|
||||
|
||||
let rows = sqlx::query(
|
||||
r#"
|
||||
WITH points AS (
|
||||
WITH segments AS (
|
||||
SELECT
|
||||
ST_SetSRID(
|
||||
ST_MakePoint($1, $2),
|
||||
4326
|
||||
) AS p1,
|
||||
|
||||
ST_SetSRID(
|
||||
ST_MakePoint($3, $4),
|
||||
4326
|
||||
) AS p2
|
||||
ordinality AS idx,
|
||||
ST_SetSRID(ST_MakePoint(lon1, lat1), 4326) AS p1,
|
||||
ST_SetSRID(ST_MakePoint(lon2, lat2), 4326) AS p2
|
||||
FROM UNNEST($1::float8[], $2::float8[], $3::float8[], $4::float8[])
|
||||
WITH ORDINALITY AS t(lon1, lat1, lon2, lat2, ordinality)
|
||||
),
|
||||
|
||||
track AS (
|
||||
SELECT
|
||||
idx,
|
||||
p1,
|
||||
p2,
|
||||
ST_MakeLine(p1, p2) AS line
|
||||
FROM points
|
||||
FROM segments
|
||||
),
|
||||
|
||||
endpoint_counties AS (
|
||||
SELECT
|
||||
t.idx,
|
||||
a.id AS from_id,
|
||||
b.id AS to_id,
|
||||
t.line
|
||||
|
|
@ -65,6 +84,7 @@ pub async fn crossings_between(
|
|||
|
||||
intersections AS (
|
||||
SELECT
|
||||
e.idx,
|
||||
e.from_id,
|
||||
e.to_id,
|
||||
e.line,
|
||||
|
|
@ -86,6 +106,7 @@ pub async fn crossings_between(
|
|||
)
|
||||
|
||||
SELECT
|
||||
idx,
|
||||
from_id,
|
||||
to_id,
|
||||
|
||||
|
|
@ -102,13 +123,13 @@ pub async fn crossings_between(
|
|||
WHERE
|
||||
ST_Dimension(intersection) = 0
|
||||
|
||||
ORDER BY fraction
|
||||
ORDER BY idx, fraction
|
||||
"#,
|
||||
)
|
||||
.bind(a.lon)
|
||||
.bind(a.lat)
|
||||
.bind(b.lon)
|
||||
.bind(b.lat)
|
||||
.bind(&lon1)
|
||||
.bind(&lat1)
|
||||
.bind(&lon2)
|
||||
.bind(&lat2)
|
||||
.fetch_all(db)
|
||||
.await?;
|
||||
|
||||
|
|
@ -125,6 +146,14 @@ pub async fn crossings_between(
|
|||
continue;
|
||||
}
|
||||
|
||||
// idx is the 1-based ordinality of the segment within the
|
||||
// batched UNNEST, i.e. segment i is (points[i - 1], points[i]).
|
||||
let idx: i64 = row.try_get("idx")?;
|
||||
let segment = (idx - 1) as usize;
|
||||
|
||||
let a = &points[segment];
|
||||
let b = &points[segment + 1];
|
||||
|
||||
let from_county_id: i64 = row.try_get("from_id")?;
|
||||
|
||||
let to_county_id: i64 = row.try_get("to_id")?;
|
||||
|
|
|
|||
32
src/main.rs
32
src/main.rs
|
|
@ -1031,27 +1031,25 @@ pub async fn process_activity_with_client(
|
|||
|
||||
let mut crossing_count = 0usize;
|
||||
|
||||
for pair in points.windows(2) {
|
||||
let crossings = geo::crossings_between(&state.db, &pair[0], &pair[1]).await?;
|
||||
let crossings = geo::crossings_for_track(&state.db, &points).await?;
|
||||
|
||||
for crossing in crossings {
|
||||
let interval = IntervalsClient::matching_interval(&activity, crossing.track_index);
|
||||
for crossing in crossings {
|
||||
let interval = IntervalsClient::matching_interval(&activity, crossing.track_index);
|
||||
|
||||
db::save_crossing(&mut tx, activity_db_id, &crossing, interval.as_ref()).await?;
|
||||
db::save_crossing(&mut tx, activity_db_id, &crossing, interval.as_ref()).await?;
|
||||
|
||||
crossing_count += 1;
|
||||
crossing_count += 1;
|
||||
|
||||
tracing::info!(
|
||||
activity_id = %activity_id,
|
||||
time = %crossing.crossing_time,
|
||||
from = crossing.from_county_id,
|
||||
to = crossing.to_county_id,
|
||||
track_index = crossing.track_index,
|
||||
lat = crossing.lat,
|
||||
lon = crossing.lon,
|
||||
"county crossing detected"
|
||||
);
|
||||
}
|
||||
tracing::info!(
|
||||
activity_id = %activity_id,
|
||||
time = %crossing.crossing_time,
|
||||
from = crossing.from_county_id,
|
||||
to = crossing.to_county_id,
|
||||
track_index = crossing.track_index,
|
||||
lat = crossing.lat,
|
||||
lon = crossing.lon,
|
||||
"county crossing detected"
|
||||
);
|
||||
}
|
||||
|
||||
db::mark_activity_processed(&mut tx, activity_db_id).await?;
|
||||
|
|
|
|||
Loading…
Reference in a new issue