Skip to content

Commit b9e33fd

Browse files
committed
perf: Reduce memory overhead in trip augmentation during ingest
Replace the bucket-and-sort approach in get_trip_rows_with_extra_time_fields with a single streaming pass over stop_times that tracks min/max stop sequence per trip, avoiding materializing full trip/stop_time lists in memory.
1 parent 11f5dad commit b9e33fd

5 files changed

Lines changed: 260 additions & 100 deletions

File tree

.github/workflows/ci.yml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
8+
jobs:
9+
install-and-lint:
10+
runs-on: ubuntu-latest
11+
steps:
12+
- uses: actions/checkout@v4
13+
14+
- uses: actions/setup-python@v5
15+
with:
16+
python-version: "3.12"
17+
18+
- name: Install Poetry
19+
run: pip install poetry
20+
21+
- name: Install dependencies
22+
run: poetry install
23+
24+
- name: Check formatting with black
25+
run: poetry run black --check .

mbta_gtfs_sqlite/feed.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
ALL_DB_FILES = [DB_FILE, DB_COMPACT_FILE]
1414
DEFAULT_INGEST_BATCH_SIZE = 300000
1515

16+
1617
@dataclass
1718
class GtfsFeed(object):
1819
archive: "MbtaGtfsArchive"

mbta_gtfs_sqlite/ingest.py

Lines changed: 37 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
from sqlalchemy.orm import Session
2-
from typing import Dict, Any, Callable, List, Type, Iterable, Union
2+
from typing import Dict, Any, Callable, Type, Iterable, Union
33
from more_itertools import ichunked
44

55
from .build import GtfsFeedDownloadResult
66
from .reader import GtfsReader
77
from .utils.time import date_from_string, seconds_from_string
8-
from .utils.decorators import listify
9-
from .utils.indexes import bucket_by
108
from .models.agency import Agency
119
from .models.base import Base
1210
from .models.calendar_attributes import CalendarAttribute
@@ -47,23 +45,32 @@ def transform_row_dict(
4745
}
4846

4947

50-
@listify
51-
def get_trip_rows_with_extra_time_fields(
52-
trip_rows: List[Dict[str, str]],
53-
stop_time_rows: List[Dict[str, str]],
54-
) -> List[Dict[str, str]]:
55-
stop_times_by_trip_id = bucket_by(stop_time_rows, "trip_id")
56-
for trip_row in trip_rows:
57-
stop_times_for_trip = sorted(
58-
stop_times_by_trip_id[trip_row["trip_id"]],
59-
key=lambda stop_time: int(stop_time["stop_sequence"]),
60-
)
61-
yield {
62-
**trip_row,
63-
"start_time": stop_times_for_trip[0]["arrival_time"],
64-
"end_time": stop_times_for_trip[-1]["arrival_time"],
65-
"stop_count": len(stop_times_for_trip),
66-
}
48+
def get_trip_extra_fields_by_trip_id(
49+
stop_time_rows: Iterable[Dict[str, str]],
50+
) -> Dict[str, Dict[str, Any]]:
51+
extra_fields_by_trip_id: Dict[str, Dict[str, Any]] = {}
52+
for stop_time_row in stop_time_rows:
53+
trip_id = stop_time_row["trip_id"]
54+
stop_sequence = int(stop_time_row["stop_sequence"])
55+
arrival_time = stop_time_row["arrival_time"]
56+
extra_fields = extra_fields_by_trip_id.get(trip_id)
57+
if extra_fields is None:
58+
extra_fields_by_trip_id[trip_id] = {
59+
"min_stop_sequence": stop_sequence,
60+
"start_time": arrival_time,
61+
"max_stop_sequence": stop_sequence,
62+
"end_time": arrival_time,
63+
"stop_count": 1,
64+
}
65+
continue
66+
extra_fields["stop_count"] += 1
67+
if stop_sequence < extra_fields["min_stop_sequence"]:
68+
extra_fields["min_stop_sequence"] = stop_sequence
69+
extra_fields["start_time"] = arrival_time
70+
if stop_sequence > extra_fields["max_stop_sequence"]:
71+
extra_fields["max_stop_sequence"] = stop_sequence
72+
extra_fields["end_time"] = arrival_time
73+
return extra_fields_by_trip_id
6774

6875

6976
def ingest_feed_info(
@@ -117,11 +124,16 @@ def ingest_rows(
117124
session.bulk_insert_mappings(model, mappings)
118125

119126

120-
def get_augmented_trip_rows(reader: GtfsReader):
121-
stop_times = list(reader.read_stop_times())
122-
trips = list(reader.read_trips())
123-
trip_rows = get_trip_rows_with_extra_time_fields(trips, stop_times)
124-
return trip_rows
127+
def get_augmented_trip_rows(reader: GtfsReader) -> Iterable[Dict[str, str]]:
128+
extra_fields_by_trip_id = get_trip_extra_fields_by_trip_id(reader.read_stop_times())
129+
for trip_row in reader.read_trips():
130+
extra_fields = extra_fields_by_trip_id[trip_row["trip_id"]]
131+
yield {
132+
**trip_row,
133+
"start_time": extra_fields["start_time"],
134+
"end_time": extra_fields["end_time"],
135+
"stop_count": extra_fields["stop_count"],
136+
}
125137

126138

127139
def ingest_gtfs_csv_into_db(

0 commit comments

Comments
 (0)