Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@L-M-Sherlock
Copy link
Contributor

Background

Currently, Anki has two main issues when determining the last review time of a card:

  1. Performance Issue: Querying the revision log (revlog) to get the last review time is expensive and slow for batch processing
  2. Accuracy Issue: Calculating the last review time from due and interval fields can be inaccurate, especially when the they have been modified by set_due_date or changed by add-ons

Changes

This PR introduces a new last_review_time field to the Card structure that directly stores the timestamp of when a card was last reviewed.

Key Changes:

  1. Schema Update: Added optional int64 last_review_time_secs = 23 to the protobuf definition
  2. Field Addition: Added last_review_time: Option<TimestampSecs> to the Rust Card struct and related data structures
  3. Storage Integration: Updated serialization/deserialization logic to handle the new field
  4. Review Logic: Set last_review_time during card answering to capture the exact review timestamp
  5. Calculation Updates: Modified functions that calculate days/seconds since last review to prioritize the new field over legacy calculation methods

Affected Areas:

  • Card protobuf definition
  • Card data structures and serialization
  • Browser table calculations
  • Scheduling logic
  • Statistics calculations
  • Sync functionality

Benefits

  1. Improved Performance: Eliminates expensive revlog queries when calculating time since last review
  2. Better Accuracy: Stores the exact review timestamp instead of inferring it from due dates and intervals
  3. Backward Compatibility: Falls back to the existing calculation methods when last_review_time is not available

fix #4091

Copy link
Contributor

@user1823 user1823 left a comment

Choose a reason for hiding this comment

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

Left one question.

Also, we need to expose this to Python (cards.py)

pub(crate) fn days_since_last_review(&self, timing: &SchedTimingToday) -> Option<u32> {
if !self.is_due_in_days() {
if let Some(last_review_time) = self.last_review_time {
Some(timing.next_day_at.elapsed_days_since(last_review_time) as u32)
Copy link
Contributor

Choose a reason for hiding this comment

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

Doesn't this include more time than the actual?
Example:

  • My next_day_at is 4 AM.
  • I reviewed the card on 7 AM today.
  • Then, I sort my cards at 9 AM.

According to my understanding, this function will calculate the days elapsed as 0.875d (21 hours) but the actual value should be 0.083 (2 hours).

Copy link
Contributor

Choose a reason for hiding this comment

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

If my thinking above is correct, it is an issue in the current code too. It will likely be fixed by using timing.now, which was probably not used earlier because timing.now wasn't actually "now" (fixed in 4040a3c).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

According to my understanding, this function will calculate the days elapsed as 0.875d (21 hours) but the actual value should be 0.083 (2 hours).

The variable's type is u32, so the value will be 0.

Copy link
Contributor

@user1823 user1823 Jun 24, 2025

Choose a reason for hiding this comment

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

Ah, ok.

But, I think it's still more logical to use timing.now if my understanding above is correct (ignore the u32 for a moment).

Edit:
Scratch that. If we use timing.now, the elapsed days won't consider the rollover time, which is not intended.
Example: The actual elapsed time is less than 24 hours but the rollover time was crossed in between. So, days_elapsed should be 1, but if we use timing.now, the days_elapsed would be 0.

self.maybe_bury_siblings(&original, &updater.config)?;
let timing = updater.timing;
let mut card = updater.into_card();
card.last_review_time = Some(answer.answered_at.as_secs());
Copy link
Contributor

Choose a reason for hiding this comment

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

This code looks OK but I am seeing a potential bug here that would be difficult to notice if introduced.

If we later accidentally change the code in such a way that last_review_time is updated before the value is used to calculate elapsed_days, the value of elapsed_days will always be 0.

So, do we need a test here?

@user1823
Copy link
Contributor

In cards.py, don't we need to update _to_backend_card too?

@dae
Copy link
Member

dae commented Jun 27, 2025

I'm not rejecting the current implementation at this stage, but I'm curious about the reasons you chose to go with seconds instead of a day number. We already adjust the due date in import to match local collection creation, so I wouldn't expect it to be too difficult to handle this case too, and we'd be saving about 15 bytes per card (which affects table scan speed and storage space). Was it just the easier approach, or were there other reasons you picked this way?

(excited to see the change land, just don't want to make a decision we regret later because it was slightly easier at the time)

@L-M-Sherlock
Copy link
Contributor Author

Because I'm not confident to handle it.

As I noticed before, the day-level due still has some problems:

https://forums.ankiweb.net/t/some-cards-due-is-negative-after-importing-decks-with-scheduling-info/55728

@dae
Copy link
Member

dae commented Jun 30, 2025

Apologies for leaving that post unanswered; I'd intended to circle back to it, but lost track of it.

I've given this some thought, and if there are bugs with the current implementation, I think it's best we try to fix them. Storing the last review time in seconds might avoid some of the existing issues, but it doesn't solve the existing bugs, and it both increases the collection size, and makes the codebase more complicated, as we're using different representations for the due date and the last review time.

Do you have reproduction steps for the issues you encountered?

Sorry to create more work for you :-(

@L-M-Sherlock
Copy link
Contributor Author

L-M-Sherlock commented Jul 5, 2025

Sorry for the delayed reply. I'm back from traveling now :)

After checking the code, I think the issue I noticed before doesn't exist (maybe I hallucinated).

I prefer seconds instead of days because it's accurate and easy to implement. And it's consistent with the result returned by time_of_last_review. Based on it, we can improve some features to use elapsed_seconds instead of elapsed_days for accurate results.

@Luc-Mcgrady
Copy link
Contributor

Luc-Mcgrady commented Jul 5, 2025

I want seconds because it would allow me to calculate intra-day intervals for my graphing addon.

Of course, my graphing addon isn't worth bloating everyones collection size if thats what would happen. But I'd still be minorly disappointed.

@dyzur
Copy link

dyzur commented Jul 5, 2025

I want seconds for intra-day intervals also

@dae
Copy link
Member

dae commented Jul 7, 2025

I want seconds because it would allow me to calculate intra-day intervals for my graphing addon.

Presumably you could calculate the intervals by examining the revlog as well, though it would be slower.

And it's consistent with the result returned by time_of_last_review

Yeah, that's a fair point. I'm still a bit on the fence about the extra storage this will require, though we could at least reduce it in a future release by upgrading the schema and moving the current custom_data fields into the table.

@dae dae merged commit 037dfa1 into ankitects:main Jul 7, 2025
1 check passed
@user1823
Copy link
Contributor

user1823 commented Jul 7, 2025

@dae, speaking of the storage space use, would it not make sense to use an abbreviation for last_review_time because storing all these characters in each card seems to be a wastage of storage space to me.

@dae
Copy link
Member

dae commented Jul 7, 2025

It uses a 3 character name when stored.

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.

The interval is incorrect after apply Set Due Date on interday/intraday learning cards when fsrs_enabled.

5 participants