Send smaller first batch (5 features) for faster first paint
Subsequent batches use the normal batch_size (default 50). This reduces server-side time-to-first-property by ~10x since only 5 features need to be serialized before the first yield.
This commit is contained in:
parent
9179456bf7
commit
e99006e2f9
2 changed files with 40 additions and 4 deletions
31
api/app.py
31
api/app.py
|
|
@ -53,6 +53,7 @@ configure_logging("api")
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
DEFAULT_BATCH_SIZE = 50
|
DEFAULT_BATCH_SIZE = 50
|
||||||
|
FIRST_BATCH_SIZE = 5
|
||||||
_rate_limit_config = RateLimitConfig.from_env()
|
_rate_limit_config = RateLimitConfig.from_env()
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -288,6 +289,7 @@ async def _stream_from_cache(
|
||||||
}) + "\n"
|
}) + "\n"
|
||||||
|
|
||||||
count = 0
|
count = 0
|
||||||
|
is_first_batch = True
|
||||||
for feature_batch in get_cached_features(query_parameters, batch_size=batch_size):
|
for feature_batch in get_cached_features(query_parameters, batch_size=batch_size):
|
||||||
# Apply decision filtering
|
# Apply decision filtering
|
||||||
if decision_filter != "everything":
|
if decision_filter != "everything":
|
||||||
|
|
@ -302,9 +304,27 @@ async def _stream_from_cache(
|
||||||
]
|
]
|
||||||
if limit and count + len(feature_batch) > limit:
|
if limit and count + len(feature_batch) > limit:
|
||||||
feature_batch = feature_batch[:limit - count]
|
feature_batch = feature_batch[:limit - count]
|
||||||
count += len(feature_batch)
|
|
||||||
if feature_batch:
|
# Split first batch into smaller primer batch and remainder
|
||||||
yield json.dumps({"type": "batch", "features": feature_batch}) + "\n"
|
if is_first_batch and len(feature_batch) > FIRST_BATCH_SIZE:
|
||||||
|
# Yield primer batch
|
||||||
|
first_features = feature_batch[:FIRST_BATCH_SIZE]
|
||||||
|
count += len(first_features)
|
||||||
|
yield json.dumps({"type": "batch", "features": first_features}) + "\n"
|
||||||
|
|
||||||
|
# Yield remainder
|
||||||
|
remaining_features = feature_batch[FIRST_BATCH_SIZE:]
|
||||||
|
count += len(remaining_features)
|
||||||
|
if remaining_features:
|
||||||
|
yield json.dumps({"type": "batch", "features": remaining_features}) + "\n"
|
||||||
|
is_first_batch = False
|
||||||
|
else:
|
||||||
|
# Normal batch yielding
|
||||||
|
count += len(feature_batch)
|
||||||
|
if feature_batch:
|
||||||
|
yield json.dumps({"type": "batch", "features": feature_batch}) + "\n"
|
||||||
|
is_first_batch = False
|
||||||
|
|
||||||
if limit and count >= limit:
|
if limit and count >= limit:
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|
@ -341,6 +361,7 @@ async def _stream_from_db(
|
||||||
try:
|
try:
|
||||||
count = 0
|
count = 0
|
||||||
batch: list[dict] = []
|
batch: list[dict] = []
|
||||||
|
current_batch_target = FIRST_BATCH_SIZE # Start with smaller first batch
|
||||||
for row in repository.stream_listings_optimized(
|
for row in repository.stream_listings_optimized(
|
||||||
query_parameters, limit=limit, page_size=batch_size
|
query_parameters, limit=limit, page_size=batch_size
|
||||||
):
|
):
|
||||||
|
|
@ -359,11 +380,13 @@ async def _stream_from_db(
|
||||||
batch.append(feature)
|
batch.append(feature)
|
||||||
count += 1
|
count += 1
|
||||||
|
|
||||||
if len(batch) >= batch_size:
|
if len(batch) >= current_batch_target:
|
||||||
if staging_key:
|
if staging_key:
|
||||||
cache_features_batch_staged(staging_key, batch)
|
cache_features_batch_staged(staging_key, batch)
|
||||||
yield json.dumps({"type": "batch", "features": batch}) + "\n"
|
yield json.dumps({"type": "batch", "features": batch}) + "\n"
|
||||||
batch = []
|
batch = []
|
||||||
|
# After first batch, use normal batch size
|
||||||
|
current_batch_target = batch_size
|
||||||
|
|
||||||
if batch:
|
if batch:
|
||||||
if staging_key:
|
if staging_key:
|
||||||
|
|
|
||||||
|
|
@ -302,3 +302,16 @@ class TestStreamingEndpoint:
|
||||||
assert feature["properties"]["qm"] is None
|
assert feature["properties"]["qm"] is None
|
||||||
assert feature["properties"]["qmprice"] is None
|
assert feature["properties"]["qmprice"] is None
|
||||||
|
|
||||||
|
def test_first_batch_is_smaller(self, client, mock_repository):
|
||||||
|
"""Test that the first batch is smaller than subsequent batches for fast first paint."""
|
||||||
|
response = client.get("/api/listing_geojson/stream?listing_type=RENT&batch_size=50&limit=10")
|
||||||
|
lines = response.text.strip().split("\n")
|
||||||
|
messages = [json.loads(line) for line in lines]
|
||||||
|
|
||||||
|
batch_messages = [m for m in messages if m["type"] == "batch"]
|
||||||
|
assert len(batch_messages) >= 1
|
||||||
|
|
||||||
|
# First batch should contain FIRST_BATCH_SIZE (5) or fewer features
|
||||||
|
from api.app import FIRST_BATCH_SIZE
|
||||||
|
assert len(batch_messages[0]["features"]) <= FIRST_BATCH_SIZE
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue