Skip to content

metadata access counter; closes #3472 - #3475

Open
petrelharp wants to merge 1 commit into
tskit-dev:mainfrom
petrelharp:metadata_cache
Open

metadata access counter; closes #3472#3475
petrelharp wants to merge 1 commit into
tskit-dev:mainfrom
petrelharp:metadata_cache

Conversation

@petrelharp

Copy link
Copy Markdown
Contributor

As lengthily discussed in #3472, if top-level metadata is large then doing ts.metadata a lot of times can be verrrrry slow, because of both the decoding and making a copy of the data (in roughly equal measure). Seeing no straightforward way around this, I'm proposing this stopgap: if someone does TreeSequence.metadata or TableCollection.metadata 20 times then they get a Warning suggesting they do something else. The warning only occurs once.

@codecov

codecov Bot commented Aug 7, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 91.68%. Comparing base (12b196a) to head (78d5026).

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3475      +/-   ##
==========================================
+ Coverage   91.67%   91.68%   +0.01%     
==========================================
  Files          38       38              
  Lines       32188    32232      +44     
  Branches     5151     5153       +2     
==========================================
+ Hits        29509    29553      +44     
  Misses       2346     2346              
  Partials      333      333              
Flag Coverage Δ
C 82.23% <ø> (ø)
c-python 77.56% <100.00%> (+0.01%) ⬆️
python-tests 96.44% <100.00%> (+0.01%) ⬆️
python-tests-no-jit 33.30% <62.50%> (+0.10%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Components Coverage Δ
Python API 98.73% <100.00%> (+<0.01%) ⬆️
Python C interface 91.25% <100.00%> (+0.01%) ⬆️
C library 91.24% <ø> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@petrelharp
petrelharp requested a review from nspope August 7, 2026 17:32
@petrelharp
petrelharp marked this pull request as ready for review August 7, 2026 17:49

@nspope nspope left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks fine by me, some minor suggestions

Comment thread python/tskit/tables.py Outdated
def metadata(self):
if self._metadata_access_counter is not None:
self._metadata_access_counter += 1
if self._metadata_access_counter > 20:

@nspope nspope Aug 10, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than hardcoding 20 in multiple places, how about defining METADATA_WARNING_COUNT in __init__.py alongside the other constants, and importing it for use here (and in the tests)?

Comment thread python/tests/test_metadata.py Outdated
# should warn after 20 times and no more after that
md = t.metadata
for _ in range(19):
print(_, t._metadata_access_counter)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

stray print?

Comment thread python/tskit/tables.py Outdated
def metadata(self):
if self._metadata_access_counter is not None:
# can't set things directly on this immutable class
builtins.object.__setattr__(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you need builtins here? object exists without importing, so I think you can just do object.__setattr__ here (and elsewhere) and remove the import. (That's done elsewhere, for example with the _initialised member of the same class)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm: I did it that way because of this bit. Looks like you're right, though, don't need it.

@nspope

nspope commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

Here's one thing which isn't wrong per se but is a bit messy: checking equality may raise the counters of the involved tree sequences/tables, to a degree that varies depending on what's compared. For example,

import msprime
import tskit

ts = msprime.sim_ancestry(3, sequence_length=1, random_seed=1024)
tables = ts.dump_tables()
tables.metadata_schema = tskit.MetadataSchema.permissive_json()
tables.metadata = {"a": "bcde", "x": [1, 2, 3, 4]}
ts = tables.tree_sequence()

# mutable vs mutable, same metadata (+0, +0)
t1, t2 = ts.dump_tables(), ts.dump_tables()
t1.assert_equals(t2)
print(t1._metadata_access_counter, t2._metadata_access_counter)  # (0, 0)

# mutable vs mutable, diff metadata (+2, +2)
t3, t4 = ts.dump_tables(), ts.dump_tables()
t4.metadata = {"a": "zzz"}
try:
    t3.assert_equals(t4)
except AssertionError:
    pass
print(t3._metadata_access_counter, t4._metadata_access_counter)  # (2, 2)

# immutable vs immutable, same metadata (+1, +1)
t5, t6 = ts.tables, ts.dump_tables().tree_sequence().tables
t5.assert_equals(t6)
print(t5._metadata_access_counter, t6._metadata_access_counter) # (1, 1)

I'm not sure there's an easy way to avoid this though (and it's just cosmetic, would just add some unavoidable noise when comparing things in a loop for instance)

@petrelharp

Copy link
Copy Markdown
Contributor Author

Good point about comparisons. I don't think I'm worried about it, though?

Thanks for the input! Will get on it.

@jeromekelleher jeromekelleher left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree this is a good least-bad option here given the dependence on the return value being a mutable copy in downstream code. I think we should make the interface "official" though and make it flexible, as there will be cases where this is really annoying and we want to turn it off.

Comment thread python/tskit/__init__.py Outdated
Comment thread python/tests/test_metadata.py
@petrelharp

Copy link
Copy Markdown
Contributor Author

I've set METADATA_ACCESS_WARNING_SIZE to 200kb. First I set it to 1Mb, but the tests were annoyingly slow. Here's some anecdata, showing number of bytes and time it takes to run the test that decodes the metadata 25 times:

n             time
1000       0.01s
10000     0.09s
100000   0.92s
1000000 9.2s

So, if we set it to 1Mb, then here we're running 12 tests that would each take 9s (in parallel, but still). However, if 200Kb is too small then we should up that. However, I think we're taking 2s per decode, so we're not warning until the user has spent 40s decoding that could be cut to 2s, so I think this is okay.

Note that I'm using the json+struct codec, but using just json is no faster.

Still TODO: documentation.

@petrelharp

Copy link
Copy Markdown
Contributor Author

Docs done. This should be ready to go.

@petrelharp

Copy link
Copy Markdown
Contributor Author

Yep - I've had a careful read through, and I think we're good.

@petrelharp

petrelharp commented Aug 11, 2026

Copy link
Copy Markdown
Contributor Author

Here's screenshots of the docs built locally, for convenience:
Screenshot From 2026-08-11 16-35-40
Screenshot From 2026-08-11 16-36-12
Screenshot From 2026-08-11 16-36-23

@nspope nspope left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Docs look good to me, a couple minor nits

Comment thread docs/metadata.md
Comment thread docs/metadata.md Outdated

Because of this, tskit will produce a Warning if the top-level metadata
is large (greater than 100Kb) and is accessed many times (more than 20 times).
This behavior can be changed, using

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
This behavior can be changed, using
This behavior can be changed, by setting

@jeromekelleher jeromekelleher left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is a good approach, but the implementation is a bit complicated. Implementing a cheap metadata_size property here is straightforward, and avoids jumping through hoops.

Comment thread python/tskit/tables.py Outdated
Comment thread python/tskit/tables.py Outdated

@property
def metadata(self):
if self._metadata_access_counter is not None:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then do

if self._ll_tables.metadata_size > METADATA_ACCESS_WARNING_SIZE:
    self._metadata_access_counter += 1

This is more efficient as it avoids creating another copy of large metadata at instantiation time, and it's more straightforward semantically as you can set the thresholds any time you want.

Comment thread python/tskit/tables.py Outdated
Comment thread python/tskit/tables.py
Comment thread python/tskit/tables.py Outdated
@petrelharp

Copy link
Copy Markdown
Contributor Author

Done! It is certainly cleaner. And hah now I see you only suggested adding metadata_size to the low-level code, not the high-level code, but well I did it anyways.

Comment thread python/tests/test_highlevel.py
@petrelharp

Copy link
Copy Markdown
Contributor Author

Okay, this is I think finally ready to go!

@jeromekelleher jeromekelleher left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Happy to merge after the typo is fixed/variable is renamed.

Comment thread python/tskit/__init__.py Outdated
#: A warning will be thrown if top-level metadata is larger than "size" bytes and is
#: accessed more that "threshhold" times. To disable, set threshhold to a large number,
#: for instance: ``tskit.METADATA_ACCESS_WARNING_THRESHOLD = 2**32``.
METADATA_ACCESS_WARNING_THRESHHOLD = 20

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I missed this the last time. I think we should change this to METADATA_ACCESS_WARNING_COUNT for symmetry with METADATA_ACCESS_WARNING_SIZE (they are both thresholds). This is backtracking a bit on what I said before, I know.

In any case, there's a typo so we'll need to change it (double "H" in threshold)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sounds good! and I have now learned more about the word "threshold"! also, tangentially, that it looks like merriam-webster has a five-year cyclical list for "word of the day":
Screenshot From 2026-08-13 11-27-22

and add metadata_size property
@petrelharp

Copy link
Copy Markdown
Contributor Author

Done!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants