Commit b01afa8
perf(cosmos): improve pkrange cache memory usage (#46297)
* perf(cosmos): share pk range cache + __slots__ + skip .upper()
1. Share CollectionRoutingMap cache across clients per endpoint.
Eliminates N-1 redundant copies when N clients target the same account.
2. Add __slots__ to Range class (64 bytes vs ~250 bytes per instance).
3. Skip .upper() when string is already uppercase.
PPCB overhead (150 clients, tracemalloc):
Original: 27.4 MB -> Patched: ~0 MB (-100%)
At customer scale (200K partitions x 152 clients): ~2.1 GB -> ~14 MB
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* perf(cosmos): add PKRange namedtuple for compact partition key range storage
Convert raw service response dicts to PKRange namedtuples in both
full refresh (_build_routing_map_from_ranges) and incremental update
(process_fetched_ranges) paths. PKRange retains only 4 fields (id,
minInclusive, maxExclusive, parents) and supports dict-style access
for backward compatibility.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: resolve pylint, mypy, cspell errors in PKRange change
- Import PKRange in _routing_map_provider_common.py (fixes all emulator tests)
- Fix namedtuple name mismatch (_PKRangeBase, not PKRange) for mypy
- Use raise-from pattern in PKRange.__getitem__ (pylint W0707)
- Move _locks_lock and _collection_locks init into __init__ (pylint W0201)
- Add 'pkrange' to cspell dictionary
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* perf(cosmos): add __slots__ to _PartitionHealthInfo + comments on Range __slots__
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: mypy type annotation + move cspell to cosmos package level
- Widen range_tuples type to List[Tuple[Any, Any]] for PKRange compatibility
- Move pkrange word to sdk/cosmos/azure-cosmos/cspell.json (not .vscode)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* test(cosmos): add integration + fault injection tests for shared cache
Integration tests (7):
- Multi-client shared cache for reads and queries
- clear_cache() transparent repopulation and cross-client propagation
- Different endpoints isolated
- PKRange full CRUD lifecycle and change feed compatibility
Fault injection tests (6 sync + 6 async):
- 410 Gone triggers cache refresh
- Partition split (410/1002) refreshes routing map
- Concurrent cache refresh with ThreadPoolExecutor/asyncio.gather
- PKRange immutability (namedtuple guarantee)
- Transient 503 during PKRange fetch with retry recovery
- clear_cache during concurrent reads (no crash/corruption)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(cosmos): address review - clear_cache identity, PKRange indexing, parents tuple
Fixes from coding agent harness review iteration 1:
F1: Fix async else branch in refresh_routing_map_provider to use
clear_cache() instead of re-creating SmartRoutingMapProvider
F2: Use dict.clear() in clear_cache() to preserve all client references
(was creating new dict, orphaning other clients' references)
F3: Clear _collection_locks under _locks_lock instead of replacing
F4: Align async clear_cache() with sync (both use .clear())
F5: PKRange.__getitem__ supports integer indexing (int/slice → super())
F6: Convert parents to tuple at construction for true immutability
F8: Fix tests to verify dict identity preserved after clear_cache
F9: Cache .upper() result to avoid double call in slow path
F11: Add changelog entry
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* chore: remove harness artifacts from tracked files
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(cosmos): resolve test failures — PKRange dict equality, test updates
- Add PKRange.__eq__ for dict comparison (existing tests compare against dicts)
- Update partition split retry tests: assert clear_cache() instead of
SmartRoutingMapProvider constructor (sync + async)
- Fix sync test .close() calls (sync CosmosClient uses context manager, not .close())
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* chore: remove stale .temp artifact
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(cosmos): session token parents.copy(), shared cache test isolation, container limits
- Fix _session.py: parents.copy() -> list(parents) for tuple compatibility
- Add url_connection + tearDown to routing_map_provider tests (cache isolation)
- Use existing test containers instead of creating new ones (25-container limit)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* chore: remove .temp artifact
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(cosmos): test fixes — PKRange field assertions, remove looping fault tests
- test_routing_map.py: only check id/minInclusive/maxExclusive (PKRange's 4 fields)
- Remove fault injection tests that loop infinitely (FaultInjectionTransport
resets counter after max_inner_count, causing retry → re-fault → retry loop)
- Keep: concurrent cache refresh, PKRange immutability, concurrent reads tests
- Remove tearDown cache clearing (conflicts with setUpClass client refs)
- Fix clear_cache repopulation test (don't assert empty between clear and read)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* test(cosmos): add async versions of all shared cache tests
- test_shared_cache_integration_async.py: 7 async integration tests
(multi-client reads/queries, clear_cache, endpoint isolation, CRUD, change feed)
- test_shared_pk_range_cache_async.py: 5 async unit tests
(cache sharing, isolation, clear_cache identity, cross-endpoint isolation)
Total async test coverage: 7 integration + 5 unit + 3 fault injection = 15 async tests
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(cosmos): async tests — drop enable_cross_partition_query, use query for cache population
- async query_items() doesn't accept enable_cross_partition_query (TypeError in aiohttp)
- async point reads don't populate the PK range cache; use a cross-partition
query to deterministically populate it before/after clear_cache().
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(cosmos): async tests — populate PK range cache via direct provider call
Async query_items doesn't reliably populate _collection_routing_map_by_item
the way sync cross-partition queries do. Add _populate_cache() helper that
calls provider.get_routing_map() directly to deterministically populate
the cache for assertions.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(cosmos): address iter-2 review — shared locks, cache release, PKRange semantics
- Per-endpoint shared collection_locks dict and locks_lock, eliminating
fragile per-instance lock state when multiple PartitionKeyRangeCache
instances target the same endpoint (sync + async).
- Add reference counting (release/__del__) on the shared cache entries
and wire release() into CosmosClient.__exit__ and __aexit__ so the
shared cache is evicted when the last client for an endpoint closes.
- Make async PartitionKeyRangeCache.clear_cache an async coroutine that
acquires the per-endpoint asyncio.Lock under the threading meta-lock;
update the two await sites in _cosmos_client_connection_async and the
affected tests (await + AsyncMock).
- _resolve_endpoint falls back to id(client) when url_connection is
unavailable (e.g. MagicMock test clients) so isolation is preserved.
- PKRange.__contains__: return False for missing fields or empty tuples
to avoid spurious membership matches against unset parents.
- PKRange.__eq__ dict branch: include parents in equality, normalizing
both sides to tuple to handle service raw dicts with list/missing
parents.
- Restore parents assertion in test_routing_map_provider with tuple
normalization on both sides.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(cosmos): pylint docstrings on _resolve_endpoint + async CRUD test populate
- Add :param/:returns/:rtype docstrings on _resolve_endpoint helper
(sync + async) to satisfy azure-pylint-guidelines-checker (C4739/41/42).
- test_pkrange_survives_full_crud_lifecycle_async: drive routing-aware
query via _populate_cache before asserting cache populated. Async point
reads/writes don't reliably populate _collection_routing_map_by_item the
way sync does.
* chore: untrack .coding-harness/ harness artifacts
* ci: retrigger pipelines (flaky test_health_check_failure_startup_async on py39 dep-checks)
* doc(cosmos): document PKRange.__contains__ truthy-presence semantics
Per iter-3 reviewer minor finding F3 — clarify that 'key in pkr' returns
False for absent or empty fields so callers can use it as a single truthy
presence check (matching the legacy raw-dict behaviour where the field
was simply missing when empty).
* chore: untrack .coding-harness/ harness artifacts (proper gitignore)
* test(cosmos): bump test_timeout_for_read_items delay 2s→3s
The test reads items across multiple physical partitions through a transport
that delays each request by N seconds, expecting cumulative delay to exceed
the 5s timeout.
With shared routing-map cache, the new delayed client inherits the routing
map populated when the test container was created, eliminating one HTTP
request from the timed path. With 2 physical partitions × 2s = 4s, the test
no longer reaches the 5s timeout and the assertion fails on the
circuit_breaker_MultiMaster job (which provisions exactly 2 partitions for
offer_throughput=11000).
Bumping per-request delay to 3s (2 partitions × 3s = 6s) makes the test
robust regardless of cache-warming state.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* chore(cosmos): address PR review comments
- Remove unused imports across test files (patch, uuid, PartitionKey,
PKRange, Range, sys, pytest_asyncio, FaultInjectionTransport,
CosmosHttpResponseError, duplicate PartitionKeyRangeCache import).
- Use CosmosClient as a context manager in tests so shared-cache
refcounting is released deterministically instead of relying on GC
(sync integration, sync fault-injection worker/reader helpers).
- Clear shared routing-map cache in tearDownClass / asyncTearDown so
module-level state does not leak across test classes in the same
process.
- Use parents=() (immutable tuple) instead of parents=[] to match the
PKRange namedtuple contract and preserve deep immutability.
- Update stale docstring/inline comments in refresh_routing_map_provider
and test docstring to reflect the in-place clear() of the shared
cache instead of the old 'create a new provider instance' wording.
- Drop the brittle sys.getsizeof(pkr) < 100 assertion from
test_range_has_slots; the __slots__ contract is already verified via
hasattr(__dict__).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* docs(cosmos): explain shared routing-map cache module-level globals
Add per-line comments above each of the five module-level globals in
both sync and async routing_map_provider.py describing:
- _shared_routing_map_cache: the actual cached routing maps shared across
every client for an endpoint
- _shared_collection_locks: per-collection single-flight refresh lock
- _shared_locks_locks: guards the creation of new collection-locks to
preserve the single-flight invariant under races
- _shared_cache_refcounts: ref-count of live clients per endpoint, used
to GC the entry when the last client closes
- _shared_cache_lock: process-wide threading.Lock guarding all four
dicts; intentionally threading (not asyncio) so it can be shared
between sync and async paths and across event loops
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(cosmos): scope async pk-range locks per event loop, reset cache between tests
Two related fixes flagged in deep review of the shared partition-key-range
cache:
F1 — async locks at module scope broke across event loops
asyncio.Lock binds to the event loop on first acquire (CPython 3.10+) and
raises 'RuntimeError: ... bound to a different event loop' if reused from
another running loop. Both _shared_locks_locks (per-endpoint meta-lock)
and _shared_collection_locks (per-collection refresh lock) held module-
level asyncio.Lock instances, which fails for:
* pytest-asyncio's default function-scoped event loop (second async
test against the same emulator endpoint hits the bug)
* re-entrant asyncio.run() (uvicorn worker reload, jupyter kernel
restart, multiprocessing fork)
Fix:
* _shared_locks_locks: asyncio.Lock -> threading.Lock. Its critical
sections are pure dict reads/writes with no awaits, so a threading
lock is identical in semantics and loop-agnostic.
* _shared_collection_locks: keyed by (loop_id, collection_id) instead
of just collection_id. _get_lock_for_collection now uses
id(asyncio.get_running_loop()) so each loop owns its own asyncio.Lock
and single-flighting is correctly scoped per loop.
F3 — no autouse fixture clearing shared globals between tests
Existing test base classes construct CosmosClient without 'with', leaving
refcount entries pinned for the test process lifetime. The new shared-
cache test files added their own cache-clear teardowns but only for
_shared_routing_map_cache, missing _shared_collection_locks,
_shared_locks_locks, and _shared_cache_refcounts; existing tests cleared
nothing. Result: order-dependent failures and flakiness in any test that
asserts on routing-map cache state or _ReadPartitionKeyRanges call counts.
Fix: autouse pytest fixture in tests/conftest.py that clears all four
globals on both sync and async modules after every test.
(F2 — clear_cache stale-write race during in-flight refresh — deferred to
a follow-up PR; needs a generation counter for a complete fix.)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Fix shared-cache test fixture to preserve dict identity
The autouse fixture cleared the _shared_routing_map_cache registry between
tests, which orphaned the inner-dict references held by long-lived
class-level CosmosClient fixtures (e.g. test_shared_cache_integration's
self.client1). The next test that constructed a second client for the
same endpoint got a brand-new inner dict, breaking the cache-sharing
invariant the tests assert via assertIs.
Now we only clear the *contents* of each per-endpoint cache dict (and
per-endpoint locks dict). The registry mappings stay intact so existing
clients continue to share the same inner objects, while the staleness
between tests that motivated the fixture is still resolved.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* F2: preserve per-collection locks across clear_cache to keep single-flight
clear_cache previously did self._collection_locks.clear() alongside the
routing-map wipe. That opened a stale-write race:
* An in-flight _fetch_routing_map holds a per-collection lock that was
just removed from the dict.
* It finishes its network call and writes into the (just-cleared)
shared cache.
* A concurrent arrival creates a brand-new lock for the same collection
and races the in-flight refresher — both can write, last wins.
Worst case: the in-flight result pre-dates the cause of clear_cache
(e.g. a 410 split notification), so a stale routing map lives in the
cache as fresh until the next force-refresh.
Fix: do not touch self._collection_locks in clear_cache. The in-flight
holder still owns its lock; the next arrival acquires the same lock and
serialises behind the in-flight write, preserving the single-flight
invariant. The locks dict is still cleaned up in release() when the
endpoint refcount hits zero.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Address xinlian12 review + fix test_multi_client_shared_cache_queries
Fixes from the @sdkReviewAgent inline comments on PR #46297 plus the CI
test failure introduced by the conftest reset fixture.
C1 — TOCTOU on _released (sync + async release()):
Move the check-and-set of self._released INSIDE the _shared_cache_lock
block. Previously two concurrent callers (e.g. __exit__ racing __del__)
could both pass the early-return guard before either set the flag, then
both decrement the refcount. Added a threaded barrier-based regression
test that demonstrates the fix.
C2 — Sync CosmosClient.close():
Added close() to sync CosmosClient mirroring the async client's close().
Now that release() manages process-global refcounts, users that don't
use 'with' need a deterministic teardown path. Delegates to __exit__.
C3 — Comment correctness:
Fixed misleading comment on _shared_cache_lock claiming sync and async
modules share state — they don't, each module has its own globals. Also
fixed the refcount comment that said clear_cache decrements (it does
not — only release() does).
C4 — _session.py:386 regression coverage:
Added focused unit tests in test_session_token_unit.py for the
list(pk_range[0].get('parents') or ()) migration: PKRange-tuple input,
None parents, empty parents, tuple parents, and the parents-then-self
walk semantics.
C5 — release() lifecycle coverage:
Added 8 sync + 4 async lifecycle tests in tests/routing/:
- construct increments refcount
- release decrements / multi-client decrement
- release evicts all four globals at zero
- release does not evict with other clients alive
- release is idempotent (sequential double-call)
- concurrent release does not double-decrement (TOCTOU regression)
- __del__ fallback releases when client teardown was skipped
- clear_cache does not change refcount
Test failure fix — test_multi_client_shared_cache_queries:
Added _populate_cache helper to the sync integration test that calls
PartitionKeyRangeCache.get_routing_map directly (mirroring the async
sibling test). The previous version asserted that
query_items(... cross_partition=True) populated _collection_routing_map_by_item,
which is an implementation detail. The autouse conftest fixture exposed
this fragility — the test had been passing only by accident due to
cache state left by earlier tests.
Teardown completeness:
Updated tearDown / tearDownClass in both routing/test_shared_pk_range_cache(_async).py
and test_shared_cache_integration(_async).py to clear ALL FOUR shared-
cache globals (_shared_routing_map_cache, _shared_collection_locks,
_shared_locks_locks, _shared_cache_refcounts) rather than only the
routing-map dict. Avoids order-dependent leaks and refcount drift.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Fix Build Analyze: pylint C4732/C4739 + cspell TOCTOU
- cosmos_client.py: disable specify-parameter-names-in-call on
__exit__(None, None, None) — sentinels are positional by Python convention.
- routing_range.py: add :param/:returns/:rtype to PKRange.__contains__ docstring.
- cspell.json: add 'toctou' to ignoreWords (used in race-condition comments).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Address xinlian Apr 24 review: sync clear_cache + retain status/throughputFraction
Address xinlian12's two latest review comments on PR #46297, plus retain two
non-routing PKR fields based on bluebird-grounded review.
clear_cache: async -> sync
- aio/routing_map_provider.py: clear_cache no longer async (no awaits inside,
uses threading.Lock + dict.clear()). Mirrors the sync release() signature.
- aio/_cosmos_client_connection_async.py: drop await from the 2 callers.
- tests/test_partition_split_retry_unit_async.py: AsyncMock -> MagicMock.
- tests/routing/test_shared_pk_range_cache_async.py,
tests/test_shared_cache_fault_injection_async.py,
tests/test_shared_cache_integration_async.py: drop await from clear_cache
call sites in async tests.
PKRange: retain status and throughputFraction
- routing_range.py: add status and throughputFraction to _PKRangeBase
namedtuple with defaults=(None, None) for back-compat. Add Status and
ThroughputFraction constants.
- collection_routing_map.py + _routing_map_provider_common.py: propagate both
fields when constructing PKRange from raw service dicts (full-load and
incremental merge paths).
Tests
- test_shared_pk_range_cache.py: add test_pkrange_contains_truthy_presence_for_parents
covering parents=() (most common production case, partition has never split).
- test_shared_pk_range_cache.py: add test_pkrange_status_and_throughput_fraction_fields_roundtrip
covering default-None back-compat plus explicit values via dict-style access
and __contains__ truthy-presence semantic.
All 143 routing/cache/split-retry tests pass locally against a live account.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Revert .gitignore changes — keep PR diff scoped to PKR cache work
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Address xinlian review: dedupe _resolve_endpoint + PKRange construction
Per xinlian's review (PR #46297): two duplications were called out:
1. _resolve_endpoint() was identical in sync and async modules.
Moved to _routing_map_provider_common.py; both modules import the
shared implementation. Prevents silent fallback-shape divergence
that would fragment the per-endpoint shared cache.
2. PKRange construction was duplicated in both code paths:
- collection_routing_map._build_routing_map_from_ranges (full build)
- _routing_map_provider_common.process_fetched_ranges (incremental merge)
Added PKRange.from_dict(raw) classmethod factory in routing_range.py;
both call sites now use it. Field-mapping policy lives in exactly one
place — adding/removing a field touches one line, not two.
Net diff: 32 lines deduplicated across 3 files. No behavior change.
All 143 existing tests in tests/routing/, tests/test_partition_split_retry_unit*,
tests/test_shared_cache_integration*, tests/test_shared_cache_fault_injection_async
still pass against tomasvaron-cdb.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: pylint C4740 — restore type annotation on _resolve_endpoint
Build Analyze pylint job (build 6228688) flagged C4740(docstring-missing-type)
on _resolve_endpoint after it was moved to _routing_map_provider_common.py
in eab73eb. The original sync/async versions had `client: Any` and
`-> str` annotations plus the matching `:type client: Any` docstring line —
those were dropped during the move.
Restored the function signature to `def _resolve_endpoint(client: Any) -> str:`
(matching the originals) and added the missing `:type client: Any` docstring
entry.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: tvaron3 <tvaron3@users.noreply.github.com>
Co-authored-by: Tomas Varon <tvaron@microsoft.com>1 parent 44cfa46 commit b01afa8
28 files changed
Lines changed: 1773 additions & 79 deletions
File tree
- sdk/cosmos/azure-cosmos
- azure/cosmos
- _routing
- aio
- aio
- tests
- routing
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
10 | 10 | | |
11 | 11 | | |
12 | 12 | | |
| 13 | + | |
13 | 14 | | |
14 | 15 | | |
15 | 16 | | |
| |||
Lines changed: 7 additions & 4 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
3591 | 3591 | | |
3592 | 3592 | | |
3593 | 3593 | | |
3594 | | - | |
| 3594 | + | |
| 3595 | + | |
3595 | 3596 | | |
3596 | 3597 | | |
3597 | 3598 | | |
| |||
3634 | 3635 | | |
3635 | 3636 | | |
3636 | 3637 | | |
3637 | | - | |
3638 | | - | |
| 3638 | + | |
| 3639 | + | |
| 3640 | + | |
| 3641 | + | |
3639 | 3642 | | |
3640 | 3643 | | |
3641 | 3644 | | |
3642 | | - | |
| 3645 | + | |
3643 | 3646 | | |
3644 | 3647 | | |
3645 | 3648 | | |
| |||
Lines changed: 11 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
50 | 50 | | |
51 | 51 | | |
52 | 52 | | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
53 | 64 | | |
54 | 65 | | |
55 | 66 | | |
| |||
Lines changed: 28 additions & 2 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
34 | 34 | | |
35 | 35 | | |
36 | 36 | | |
| 37 | + | |
37 | 38 | | |
38 | 39 | | |
39 | 40 | | |
| |||
122 | 123 | | |
123 | 124 | | |
124 | 125 | | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
125 | 151 | | |
126 | 152 | | |
127 | 153 | | |
| |||
186 | 212 | | |
187 | 213 | | |
188 | 214 | | |
189 | | - | |
| 215 | + | |
190 | 216 | | |
191 | 217 | | |
192 | 218 | | |
| |||
209 | 235 | | |
210 | 236 | | |
211 | 237 | | |
212 | | - | |
| 238 | + | |
213 | 239 | | |
214 | 240 | | |
215 | 241 | | |
| |||
Lines changed: 142 additions & 12 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
24 | 24 | | |
25 | 25 | | |
26 | 26 | | |
| 27 | + | |
27 | 28 | | |
28 | 29 | | |
29 | 30 | | |
30 | 31 | | |
31 | 32 | | |
32 | 33 | | |
| 34 | + | |
33 | 35 | | |
34 | 36 | | |
35 | 37 | | |
| |||
41 | 43 | | |
42 | 44 | | |
43 | 45 | | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
44 | 100 | | |
45 | 101 | | |
46 | 102 | | |
| |||
64 | 120 | | |
65 | 121 | | |
66 | 122 | | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
67 | 162 | | |
68 | | - | |
69 | | - | |
70 | | - | |
71 | | - | |
72 | | - | |
73 | | - | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
74 | 197 | | |
75 | 198 | | |
76 | | - | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
77 | 204 | | |
78 | 205 | | |
79 | | - | |
| 206 | + | |
80 | 207 | | |
81 | 208 | | |
82 | | - | |
83 | | - | |
84 | | - | |
85 | | - | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
86 | 216 | | |
87 | 217 | | |
88 | 218 | | |
| |||
Lines changed: 5 additions & 2 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
27 | 27 | | |
28 | 28 | | |
29 | 29 | | |
30 | | - | |
| 30 | + | |
31 | 31 | | |
32 | 32 | | |
33 | 33 | | |
| |||
288 | 288 | | |
289 | 289 | | |
290 | 290 | | |
291 | | - | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
292 | 295 | | |
293 | 296 | | |
294 | 297 | | |
| |||
0 commit comments