remove the stats table

The stats table was how the early non-SQL versions of Anki kept track of
statistics, before there was a revision log. It is being removed because:

- it's not possible to show the statistics for a subset of the deck
- it can't meaningfully be copied on import/export
- it makes it harder to implement sync merging

Implications:

- graphs and deck stats roughly 1.5-3x longer than before, but we'll have the
  ability to generate stats for subsections of the deck, and it's not time
  critical code
- people who've been using anki since the very early days may notice a drop in
  statistics, as early repetitions were recorded in the stats table but the
  revlog didn't exist at that point.
- due bugs in old syncs and imports/exports, the stats and revlog may not
  match numbers exactly

To remove it, the following changes have been made:

- the graphs and deck stats now generate their data entirely from the revlog
- there are no stats to keep track of how many cards we've answered, so we
  pull that information from the revlog in reset()
- we remove _globalStats and _dailyStats from the deck
- we check if a day rollover has occurred using failedCutoff instead
- we remove the getStats() routine
- the ETA code is currently disabled
- timeboxing routines use repsToday instead of stats
- remove stats delete from export
- remove stats table and index in upgrade
- remove stats syncing and globalStats refresh pre-sync
- remove stats count check in fullSync check, which was redundant anyway
- update unit tests

Also:

- newCountToday -> newCount, to bring it in line with revCount&failedCount
  which also reflect the currently due count
- newCount -> newAvail
- timeboxing routines renamed since the old names were confusingly similar to
  refreshSession() which does something different

Todo:

- update newSeenToday & repsToday when answering a card
- reimplement eta
This commit is contained in:
Damien Elmes 2011-02-19 10:06:00 +09:00
parent 9ce60d5e3a
commit 855de47ffe
7 changed files with 54 additions and 359 deletions

View file

@ -13,7 +13,6 @@ from anki.utils import parseTags, tidyHTML, genID, ids2str, hexifyID, \
canonifyTags, joinTags, addTags, checksum, fieldChecksum canonifyTags, joinTags, addTags, checksum, fieldChecksum
from anki.history import CardHistoryEntry from anki.history import CardHistoryEntry
from anki.models import Model, CardModel, formatQA from anki.models import Model, CardModel, formatQA
from anki.stats import dailyStats, globalStats, genToday
from anki.fonts import toPlatformFont from anki.fonts import toPlatformFont
from anki.tags import initTagTables, tagIds from anki.tags import initTagTables, tagIds
from operator import itemgetter from operator import itemgetter
@ -26,7 +25,7 @@ from anki.upgrade import upgradeSchema, updateIndices, upgradeDeck, DECK_VERSION
import anki.latex # sets up hook import anki.latex # sets up hook
# ensure all the DB metadata in other files is loaded before proceeding # ensure all the DB metadata in other files is loaded before proceeding
import anki.models, anki.facts, anki.cards, anki.stats import anki.models, anki.facts, anki.cards
import anki.history, anki.media import anki.history, anki.media
# rest # rest
@ -206,6 +205,13 @@ class Deck(object):
# global counts # global counts
self.cardCount = self.s.scalar("select count(*) from cards") self.cardCount = self.s.scalar("select count(*) from cards")
self.factCount = self.s.scalar("select count(*) from facts") self.factCount = self.s.scalar("select count(*) from facts")
# day counts
(self.repsToday, self.newSeenToday) = self.s.first("""
select count(), sum(case when reps = 1 then 1 else 0 end) from reviewHistory
where time > :t""", t=self.failedCutoff-86400)
self.newSeenToday = self.newSeenToday or 0
print "newSeenToday in answer(), reset called twice"
print "newSeenToday needs to account for drill mode too."
# due counts # due counts
self.rebuildFailedCount() self.rebuildFailedCount()
self.rebuildRevCount() self.rebuildRevCount()
@ -251,7 +257,7 @@ class Deck(object):
"and combinedDue < :lim"), lim=self.dueCutoff) "and combinedDue < :lim"), lim=self.dueCutoff)
def _rebuildNewCount(self): def _rebuildNewCount(self):
self.newCount = self.s.scalar( self.newAvail = self.s.scalar(
self.cardLimit( self.cardLimit(
"newActive", "newInactive", "newActive", "newInactive",
"select count(*) from cards c where type = 2 " "select count(*) from cards c where type = 2 "
@ -260,9 +266,9 @@ class Deck(object):
self.spacedCards = [] self.spacedCards = []
def _updateNewCountToday(self): def _updateNewCountToday(self):
self.newCountToday = max(min( self.newCount = max(min(
self.newCount, self.newCardsPerDay - self.newAvail, self.newCardsPerDay -
self.newCardsDoneToday()), 0) self.newSeenToday), 0)
def _fillFailedQueue(self): def _fillFailedQueue(self):
if self.failedSoonCount and not self.failedQueue: if self.failedSoonCount and not self.failedQueue:
@ -285,7 +291,7 @@ limit %d""" % (self.revOrder(), self.queueLimit)), lim=self.dueCutoff)
self.revQueue.reverse() self.revQueue.reverse()
def _fillNewQueue(self): def _fillNewQueue(self):
if self.newCountToday and not self.newQueue and not self.spacedCards: if self.newCount and not self.newQueue and not self.spacedCards:
self.newQueue = self.s.all( self.newQueue = self.s.all(
self.cardLimit( self.cardLimit(
"newActive", "newInactive", """ "newActive", "newInactive", """
@ -355,7 +361,7 @@ produce the problem.
Counts %d %d %d Counts %d %d %d
Queue %d %d %d Queue %d %d %d
Card info: %d %d %d Card info: %d %d %d
New type: %s""" % (self.failedSoonCount, self.revCount, self.newCountToday, New type: %s""" % (self.failedSoonCount, self.revCount, self.newCount,
len(self.failedQueue), len(self.revQueue), len(self.failedQueue), len(self.revQueue),
len(self.newQueue), len(self.newQueue),
card.reps, card.successive, oldSuc, `newType`)) card.reps, card.successive, oldSuc, `newType`))
@ -443,9 +449,6 @@ when type >= 0 then relativeDelay else relativeDelay - 3 end)
self.dueCutoff = time.time() self.dueCutoff = time.time()
def reset(self): def reset(self):
# setup global/daily stats
self._globalStats = globalStats(self)
self._dailyStats = dailyStats(self)
# recheck counts # recheck counts
self.rebuildCounts() self.rebuildCounts()
# empty queues; will be refilled by getCard() # empty queues; will be refilled by getCard()
@ -455,9 +458,9 @@ when type >= 0 then relativeDelay else relativeDelay - 3 end)
self.spacedFacts = {} self.spacedFacts = {}
# determine new card distribution # determine new card distribution
if self.newCardSpacing == NEW_CARDS_DISTRIBUTE: if self.newCardSpacing == NEW_CARDS_DISTRIBUTE:
if self.newCountToday: if self.newCount:
self.newCardModulus = ( self.newCardModulus = (
(self.newCountToday + self.revCount) / self.newCountToday) (self.newCount + self.revCount) / self.newCount)
# if there are cards to review, ensure modulo >= 2 # if there are cards to review, ensure modulo >= 2
if self.revCount: if self.revCount:
self.newCardModulus = max(2, self.newCardModulus) self.newCardModulus = max(2, self.newCardModulus)
@ -474,7 +477,7 @@ when type >= 0 then relativeDelay else relativeDelay - 3 end)
def checkDay(self): def checkDay(self):
# check if the day has rolled over # check if the day has rolled over
if genToday(self) != self._dailyStats.day: if time.time() > self.failedCutoff:
self.updateCutoff() self.updateCutoff()
self.reset() self.reset()
@ -531,7 +534,7 @@ order by combinedDue limit %d""" % self.queueLimit), lim=self.dueCutoff)
self.scheduler = "learnMore" self.scheduler = "learnMore"
def _rebuildLearnMoreCount(self): def _rebuildLearnMoreCount(self):
self.newCount = self.s.scalar( self.newAvail = self.s.scalar(
self.cardLimit( self.cardLimit(
"newActive", "newInactive", "newActive", "newInactive",
"select count(*) from cards c where type = 2 " "select count(*) from cards c where type = 2 "
@ -539,7 +542,7 @@ order by combinedDue limit %d""" % self.queueLimit), lim=self.dueCutoff)
self.spacedCards = [] self.spacedCards = []
def _updateLearnMoreCountToday(self): def _updateLearnMoreCountToday(self):
self.newCountToday = self.newCount self.newCount = self.newAvail
# Cramming # Cramming
########################################################################## ##########################################################################
@ -611,8 +614,8 @@ order by combinedDue limit %d""" % self.queueLimit), lim=self.dueCutoff)
self.failedCramQueue.pop() self.failedCramQueue.pop()
def _rebuildCramNewCount(self): def _rebuildCramNewCount(self):
self.newAvail = 0
self.newCount = 0 self.newCount = 0
self.newCountToday = 0
def _cramCardLimit(self, active, inactive, sql): def _cramCardLimit(self, active, inactive, sql):
# inactive is (currently) ignored # inactive is (currently) ignored
@ -683,7 +686,7 @@ limit %s""" % (self.cramOrder, self.queueLimit)))
if self.revNoSpaced(): if self.revNoSpaced():
return self.revQueue[-1][0] return self.revQueue[-1][0]
# new cards left? # new cards left?
if self.newCountToday: if self.newCount:
id = self.getNewCard() id = self.getNewCard()
if id: if id:
return id return id
@ -706,7 +709,7 @@ limit %s""" % (self.cramOrder, self.queueLimit)))
def _timeForNewCard(self): def _timeForNewCard(self):
"True if it's time to display a new card when distributing." "True if it's time to display a new card when distributing."
if not self.newCountToday: if not self.newCount:
return False return False
if self.newCardSpacing == NEW_CARDS_LAST: if self.newCardSpacing == NEW_CARDS_LAST:
return False return False
@ -797,7 +800,7 @@ limit %s""" % (self.cramOrder, self.queueLimit)))
elif oldQueue == 1: elif oldQueue == 1:
self.revCount -= 1 self.revCount -= 1
else: else:
self.newCount -= 1 self.newAvail -= 1
# card stats # card stats
anki.cards.Card.updateStats(card, ease, oldState) anki.cards.Card.updateStats(card, ease, oldState)
# update type & ensure past cutoff # update type & ensure past cutoff
@ -811,9 +814,6 @@ limit %s""" % (self.cramOrder, self.queueLimit)))
# save # save
card.combinedDue = card.due card.combinedDue = card.due
card.toDB(self.s) card.toDB(self.s)
# global/daily stats
anki.stats.updateAllStats(self.s, self._globalStats, self._dailyStats,
card, ease, oldState)
# review history # review history
entry = CardHistoryEntry(card, ease, lastDelay) entry = CardHistoryEntry(card, ease, lastDelay)
entry.writeSQL(self.s) entry.writeSQL(self.s)
@ -1187,13 +1187,6 @@ where type between -3 and -1 and id in %s""" %
select 1 from cards where combinedDue < :now select 1 from cards where combinedDue < :now
and type between 0 and 1 limit 1""", now=self.dueCutoff) and type between 0 and 1 limit 1""", now=self.dueCutoff)
def newCardsDoneToday(self):
return (self._dailyStats.newEase0 +
self._dailyStats.newEase1 +
self._dailyStats.newEase2 +
self._dailyStats.newEase3 +
self._dailyStats.newEase4)
def spacedCardCount(self): def spacedCardCount(self):
"Number of spaced cards." "Number of spaced cards."
return self.s.scalar(""" return self.s.scalar("""
@ -1251,22 +1244,9 @@ combinedDue > :now and due < :now""", now=time.time())
# Stats # Stats
########################################################################## ##########################################################################
def getStats(self, short=False):
"Return some commonly needed stats."
stats = anki.stats.getStats(self.s, self._globalStats, self._dailyStats)
# add scheduling related stats
stats['new'] = self.newCountToday
stats['failed'] = self.failedSoonCount
stats['rev'] = self.revCount
if stats['dAverageTime']:
stats['timeLeft'] = anki.utils.fmtTimeSpan(
self.getETA(stats), pad=0, point=1, short=short)
else:
stats['timeLeft'] = _("Unknown")
return stats
def getETA(self, stats): def getETA(self, stats):
# rev + new cards first, account for failures # rev + new cards first, account for failures
import traceback; traceback.print_stack()
count = stats['rev'] + stats['new'] count = stats['rev'] + stats['new']
count *= 1 + stats['gYoungNo%'] / 100.0 count *= 1 + stats['gYoungNo%'] / 100.0
left = count * stats['dAverageTime'] left = count * stats['dAverageTime']
@ -1377,7 +1357,8 @@ where factId = :fid and cardModelId = :cmid""",
fact.created+0.0001*cardModel.ordinal) fact.created+0.0001*cardModel.ordinal)
self.updateCardTags([card.id]) self.updateCardTags([card.id])
self.cardCount += 1 self.cardCount += 1
self.newCount += 1 raise Exception("incorrect; not checking selective study")
self.newAvail += 1
ids.append(card.id) ids.append(card.id)
if ids: if ids:
@ -2794,18 +2775,21 @@ select id from facts where spaceUntil like :_ff_%d escape '\\'""" % c
assert '\\' not in n assert '\\' not in n
return n return n
# Session handling # Timeboxing
########################################################################## ##########################################################################
def startSession(self): def startTimebox(self):
self.lastSessionStart = self.sessionStartTime self.lastSessionStart = self.sessionStartTime
self.sessionStartTime = time.time() self.sessionStartTime = time.time()
self.sessionStartReps = self.getStats()['dTotal'] self.sessionStartReps = self.repsToday
def stopSession(self): def stopTimebox(self):
self.sessionStartTime = 0 self.sessionStartTime = 0
def sessionLimitReached(self): def timeboxStarted(self):
return self.sessionStartTime
def timeboxReached(self):
if not self.sessionStartTime: if not self.sessionStartTime:
# not started # not started
return False return False
@ -2813,7 +2797,7 @@ select id from facts where spaceUntil like :_ff_%d escape '\\'""" % c
(self.sessionStartTime + self.sessionTimeLimit)): (self.sessionStartTime + self.sessionTimeLimit)):
return True return True
if (self.sessionRepLimit and self.sessionRepLimit <= if (self.sessionRepLimit and self.sessionRepLimit <=
self.getStats()['dTotal'] - self.sessionStartReps): self.repsToday - self.sessionStartReps):
return True return True
return False return False
@ -3606,8 +3590,6 @@ class DeckStorage(object):
raise e raise e
if not rebuild: if not rebuild:
# minimal startup # minimal startup
deck._globalStats = globalStats(deck)
deck._dailyStats = dailyStats(deck)
return deck return deck
oldMod = deck.modified oldMod = deck.modified
# fix a bug with current model being unset # fix a bug with current model being unset

View file

@ -126,8 +126,6 @@ relativeDelay = 2,
combinedDue = created, combinedDue = created,
modified = :now modified = :now
""", now=time.time()) """, now=time.time())
self.newDeck.s.statement("""
delete from stats""")
# media # media
if self.includeMedia: if self.includeMedia:
server.deck.mediaPrefix = "" server.deck.mediaPrefix = ""

View file

@ -2,12 +2,9 @@
# Copyright: Damien Elmes <anki@ichi2.net> # Copyright: Damien Elmes <anki@ichi2.net>
# License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html # License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html
import os, sys, time import os, sys, time, datetime
import anki.stats
from anki.lang import _ from anki.lang import _
import datetime
#colours for graphs #colours for graphs
dueYoungC = "#ffb380" dueYoungC = "#ffb380"
dueMatureC = "#ff5555" dueMatureC = "#ff5555"
@ -100,7 +97,8 @@ from cards c where relativeDelay = 1 and type >= 0 and interval > 21"""
dayReps = self.getDayReps() dayReps = self.getDayReps()
todaydt = self.deck._dailyStats.day todaydt = datetime.datetime.utcfromtimestamp(
time.time() - self.deck.utcOffset).date()
for dest, source in [("dayRepsNew", "combinedNewReps"), for dest, source in [("dayRepsNew", "combinedNewReps"),
("dayRepsYoung", "combinedYoungReps"), ("dayRepsYoung", "combinedYoungReps"),
("dayRepsMature", "matureReps")]: ("dayRepsMature", "matureReps")]:

View file

@ -2,246 +2,13 @@
# Copyright: Damien Elmes <anki@ichi2.net> # Copyright: Damien Elmes <anki@ichi2.net>
# License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html # License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html
# we track statistics over the life of the deck, and per-day import time, sys, os, datetime
STATS_LIFE = 0
STATS_DAY = 1
import unicodedata, time, sys, os, datetime
import anki, anki.utils import anki, anki.utils
from datetime import date
from anki.db import * from anki.db import *
from anki.lang import _, ngettext from anki.lang import _, ngettext
from anki.utils import canonifyTags, ids2str from anki.utils import canonifyTags, ids2str
from anki.hooks import runFilter from anki.hooks import runFilter
# Tracking stats on the DB
##########################################################################
statsTable = Table(
'stats', metadata,
Column('id', Integer, primary_key=True),
Column('type', Integer, nullable=False),
Column('day', Date, nullable=False),
Column('reps', Integer, nullable=False, default=0),
Column('averageTime', Float, nullable=False, default=0),
Column('reviewTime', Float, nullable=False, default=0),
# next two columns no longer used
Column('distractedTime', Float, nullable=False, default=0),
Column('distractedReps', Integer, nullable=False, default=0),
Column('newEase0', Integer, nullable=False, default=0),
Column('newEase1', Integer, nullable=False, default=0),
Column('newEase2', Integer, nullable=False, default=0),
Column('newEase3', Integer, nullable=False, default=0),
Column('newEase4', Integer, nullable=False, default=0),
Column('youngEase0', Integer, nullable=False, default=0),
Column('youngEase1', Integer, nullable=False, default=0),
Column('youngEase2', Integer, nullable=False, default=0),
Column('youngEase3', Integer, nullable=False, default=0),
Column('youngEase4', Integer, nullable=False, default=0),
Column('matureEase0', Integer, nullable=False, default=0),
Column('matureEase1', Integer, nullable=False, default=0),
Column('matureEase2', Integer, nullable=False, default=0),
Column('matureEase3', Integer, nullable=False, default=0),
Column('matureEase4', Integer, nullable=False, default=0))
class Stats(object):
def __init__(self):
self.day = None
self.reps = 0
self.averageTime = 0
self.reviewTime = 0
self.distractedTime = 0
self.distractedReps = 0
self.newEase0 = 0
self.newEase1 = 0
self.newEase2 = 0
self.newEase3 = 0
self.newEase4 = 0
self.youngEase0 = 0
self.youngEase1 = 0
self.youngEase2 = 0
self.youngEase3 = 0
self.youngEase4 = 0
self.matureEase0 = 0
self.matureEase1 = 0
self.matureEase2 = 0
self.matureEase3 = 0
self.matureEase4 = 0
def fromDB(self, s, id):
r = s.first("select * from stats where id = :id", id=id)
(self.id,
self.type,
self.day,
self.reps,
self.averageTime,
self.reviewTime,
self.distractedTime,
self.distractedReps,
self.newEase0,
self.newEase1,
self.newEase2,
self.newEase3,
self.newEase4,
self.youngEase0,
self.youngEase1,
self.youngEase2,
self.youngEase3,
self.youngEase4,
self.matureEase0,
self.matureEase1,
self.matureEase2,
self.matureEase3,
self.matureEase4) = r
self.day = datetime.date(*[int(i) for i in self.day.split("-")])
def create(self, s, type, day):
self.type = type
self.day = day
s.execute("""insert into stats
(type, day, reps, averageTime, reviewTime, distractedTime, distractedReps,
newEase0, newEase1, newEase2, newEase3, newEase4, youngEase0, youngEase1,
youngEase2, youngEase3, youngEase4, matureEase0, matureEase1, matureEase2,
matureEase3, matureEase4) values (:type, :day, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)""", self.__dict__)
self.id = s.scalar(
"select id from stats where type = :type and day = :day",
type=type, day=day)
def toDB(self, s):
assert self.id
s.execute("""update stats set
type=:type,
day=:day,
reps=:reps,
averageTime=:averageTime,
reviewTime=:reviewTime,
newEase0=:newEase0,
newEase1=:newEase1,
newEase2=:newEase2,
newEase3=:newEase3,
newEase4=:newEase4,
youngEase0=:youngEase0,
youngEase1=:youngEase1,
youngEase2=:youngEase2,
youngEase3=:youngEase3,
youngEase4=:youngEase4,
matureEase0=:matureEase0,
matureEase1=:matureEase1,
matureEase2=:matureEase2,
matureEase3=:matureEase3,
matureEase4=:matureEase4
where id = :id""", self.__dict__)
mapper(Stats, statsTable)
def genToday(deck):
return datetime.datetime.utcfromtimestamp(
time.time() - deck.utcOffset).date()
def updateAllStats(s, gs, ds, card, ease, oldState):
"Update global and daily statistics."
updateStats(s, gs, card, ease, oldState)
updateStats(s, ds, card, ease, oldState)
def updateStats(s, stats, card, ease, oldState):
stats.reps += 1
delay = card.totalTime()
if delay >= 60:
stats.reviewTime += 60
else:
stats.reviewTime += delay
stats.averageTime = (
stats.reviewTime / float(stats.reps))
# update eases
attr = oldState + "Ease%d" % ease
setattr(stats, attr, getattr(stats, attr) + 1)
stats.toDB(s)
def globalStats(deck):
s = deck.s
type = STATS_LIFE
today = genToday(deck)
id = s.scalar("select id from stats where type = :type",
type=type)
stats = Stats()
if id:
stats.fromDB(s, id)
return stats
else:
stats.create(s, type, today)
stats.type = type
return stats
def dailyStats(deck):
s = deck.s
type = STATS_DAY
today = genToday(deck)
id = s.scalar("select id from stats where type = :type and day = :day",
type=type, day=today)
stats = Stats()
if id:
stats.fromDB(s, id)
return stats
else:
stats.create(s, type, today)
return stats
def summarizeStats(stats, pre=""):
"Generate percentages and total counts for STATS. Optionally prefix."
cardTypes = ("new", "young", "mature")
h = {}
# total counts
###############
for type in cardTypes:
# total yes/no for type, eg. gNewYes
h[pre + type.capitalize() + "No"] = (getattr(stats, type + "Ease0") +
getattr(stats, type + "Ease1"))
h[pre + type.capitalize() + "Yes"] = (getattr(stats, type + "Ease2") +
getattr(stats, type + "Ease3") +
getattr(stats, type + "Ease4"))
# total for type, eg. gNewTotal
h[pre + type.capitalize() + "Total"] = (
h[pre + type.capitalize() + "No"] +
h[pre + type.capitalize() + "Yes"])
# total yes/no, eg. gYesTotal
for answer in ("yes", "no"):
num = 0
for type in cardTypes:
num += h[pre + type.capitalize() + answer.capitalize()]
h[pre + answer.capitalize() + "Total"] = num
# total over all, eg. gTotal
num = 0
for type in cardTypes:
num += h[pre + type.capitalize() + "Total"]
h[pre + "Total"] = num
# percentages
##############
for type in cardTypes:
# total yes/no % by type, eg. gNewYes%
for answer in ("yes", "no"):
setPercentage(h, pre + type.capitalize() + answer.capitalize(),
pre + type.capitalize())
for answer in ("yes", "no"):
# total yes/no, eg. gYesTotal%
setPercentage(h, pre + answer.capitalize() + "Total", pre)
h[pre + 'AverageTime'] = stats.averageTime
h[pre + 'ReviewTime'] = stats.reviewTime
return h
def setPercentage(h, a, b):
try:
h[a + "%"] = (h[a] / float(h[b + "Total"])) * 100
except ZeroDivisionError:
h[a + "%"] = 0
def getStats(s, gs, ds):
"Return a handy dictionary exposing a number of internal stats."
h = {}
h.update(summarizeStats(gs, "g"))
h.update(summarizeStats(ds, "d"))
return h
# Card stats # Card stats
########################################################################## ##########################################################################
@ -295,7 +62,7 @@ class CardStats(object):
s = anki.utils.fmtTimeSpan(time.time() - tm) s = anki.utils.fmtTimeSpan(time.time() - tm)
return _("%s ago") % s return _("%s ago") % s
# Deck stats (specific to the 'sched' scheduler) # Deck stats
########################################################################## ##########################################################################
class DeckStats(object): class DeckStats(object):
@ -355,7 +122,7 @@ class DeckStats(object):
'partOf' : nYes, 'partOf' : nYes,
'totalSum' : nAll } + "<br><br>") 'totalSum' : nAll } + "<br><br>")
# average pending time # average pending time
existing = d.cardCount - d.newCountToday existing = d.cardCount - d.newCount
def tr(a, b): def tr(a, b):
return "<tr><td>%s</td><td align=right>%s</td></tr>" % (a, b) return "<tr><td>%s</td><td align=right>%s</td></tr>" % (a, b)
def repsPerDay(reps,days): def repsPerDay(reps,days):

View file

@ -11,9 +11,7 @@ from anki.errors import *
from anki.models import Model, FieldModel, CardModel from anki.models import Model, FieldModel, CardModel
from anki.facts import Fact, Field from anki.facts import Fact, Field
from anki.cards import Card from anki.cards import Card
from anki.stats import Stats, globalStats
from anki.history import CardHistoryEntry from anki.history import CardHistoryEntry
from anki.stats import globalStats
from anki.utils import ids2str, hexifyID, checksum from anki.utils import ids2str, hexifyID, checksum
from anki.media import mediaFiles from anki.media import mediaFiles
from anki.lang import _ from anki.lang import _
@ -114,7 +112,6 @@ class SyncTools(object):
def genPayload(self, summaries): def genPayload(self, summaries):
(lsum, rsum) = summaries (lsum, rsum) = summaries
self.preSyncRefresh()
payload = {} payload = {}
# first, handle models, facts and cards # first, handle models, facts and cards
for key in KEYS: for key in KEYS:
@ -125,7 +122,6 @@ class SyncTools(object):
self.deleteObjsFromKey(diff[3], key) self.deleteObjsFromKey(diff[3], key)
# handle the remainder # handle the remainder
if self.localTime > self.remoteTime: if self.localTime > self.remoteTime:
payload['stats'] = self.bundleStats()
payload['history'] = self.bundleHistory() payload['history'] = self.bundleHistory()
payload['sources'] = self.bundleSources() payload['sources'] = self.bundleSources()
# finally, set new lastSync and bundle the deck info # finally, set new lastSync and bundle the deck info
@ -134,7 +130,6 @@ class SyncTools(object):
def applyPayload(self, payload): def applyPayload(self, payload):
reply = {} reply = {}
self.preSyncRefresh()
# model, facts and cards # model, facts and cards
for key in KEYS: for key in KEYS:
k = 'added-' + key k = 'added-' + key
@ -146,14 +141,12 @@ class SyncTools(object):
self.deleteObjsFromKey(payload['deleted-' + key], key) self.deleteObjsFromKey(payload['deleted-' + key], key)
# send back deck-related stuff if it wasn't sent to us # send back deck-related stuff if it wasn't sent to us
if not 'deck' in payload: if not 'deck' in payload:
reply['stats'] = self.bundleStats()
reply['history'] = self.bundleHistory() reply['history'] = self.bundleHistory()
reply['sources'] = self.bundleSources() reply['sources'] = self.bundleSources()
# finally, set new lastSync and bundle the deck info # finally, set new lastSync and bundle the deck info
reply['deck'] = self.bundleDeck() reply['deck'] = self.bundleDeck()
else: else:
self.updateDeck(payload['deck']) self.updateDeck(payload['deck'])
self.updateStats(payload['stats'])
self.updateHistory(payload['history']) self.updateHistory(payload['history'])
if 'sources' in payload: if 'sources' in payload:
self.updateSources(payload['sources']) self.updateSources(payload['sources'])
@ -172,7 +165,6 @@ class SyncTools(object):
# deck # deck
if 'deck' in reply: if 'deck' in reply:
self.updateDeck(reply['deck']) self.updateDeck(reply['deck'])
self.updateStats(reply['stats'])
self.updateHistory(reply['history']) self.updateHistory(reply['history'])
if 'sources' in reply: if 'sources' in reply:
self.updateSources(reply['sources']) self.updateSources(reply['sources'])
@ -194,10 +186,6 @@ class SyncTools(object):
self.deck.s.refresh(self.deck) self.deck.s.refresh(self.deck)
self.deck.currentModel self.deck.currentModel
def preSyncRefresh(self):
# ensure global stats are available (queue may not be built)
self.deck._globalStats = globalStats(self.deck)
# Summaries # Summaries
########################################################################## ##########################################################################
@ -553,7 +541,7 @@ values
def deleteCards(self, ids): def deleteCards(self, ids):
self.deck.deleteCards(ids) self.deck.deleteCards(ids)
# Deck/stats/history # Deck/history
########################################################################## ##########################################################################
def bundleDeck(self): def bundleDeck(self):
@ -595,42 +583,6 @@ insert or replace into deckVars
del deck['meta'] del deck['meta']
self.applyDict(self.deck, deck) self.applyDict(self.deck, deck)
def bundleStats(self):
def bundleStat(stat):
s = self.dictFromObj(stat)
s['day'] = s['day'].toordinal()
del s['id']
return s
lastDay = date.fromtimestamp(max(0, self.deck.lastSync - 60*60*24))
ids = self.deck.s.column0(
"select id from stats where type = 1 and day >= :day", day=lastDay)
stat = Stats()
def statFromId(id):
stat.fromDB(self.deck.s, id)
return stat
stats = {
'global': bundleStat(self.deck._globalStats),
'daily': [bundleStat(statFromId(id)) for id in ids],
}
return stats
def updateStats(self, stats):
stats['global']['day'] = date.fromordinal(stats['global']['day'])
self.applyDict(self.deck._globalStats, stats['global'])
self.deck._globalStats.toDB(self.deck.s)
for record in stats['daily']:
record['day'] = date.fromordinal(record['day'])
stat = Stats()
id = self.deck.s.scalar("select id from stats where "
"type = :type and day = :day",
type=1, day=record['day'])
if id:
stat.fromDB(self.deck.s, id)
else:
stat.create(self.deck.s, 1, record['day'])
self.applyDict(stat, record)
stat.toDB(self.deck.s)
def bundleHistory(self): def bundleHistory(self):
return self.realLists(self.deck.s.all(""" return self.realLists(self.deck.s.all("""
select cardId, time, lastInterval, nextInterval, ease, delay, select cardId, time, lastInterval, nextInterval, ease, delay,
@ -886,10 +838,6 @@ and cards.id in %s""" % ids2str([c[0] for c in cards])))
ls=self.deck.lastSync) > 1000: ls=self.deck.lastSync) > 1000:
return True return True
lastDay = date.fromtimestamp(max(0, self.deck.lastSync - 60*60*24)) lastDay = date.fromtimestamp(max(0, self.deck.lastSync - 60*60*24))
if self.deck.s.scalar(
"select count() from stats where day >= :day",
day=lastDay) > 100:
return True
return False return False
def prepareFullSync(self): def prepareFullSync(self):

View file

@ -2,7 +2,7 @@
# Copyright: Damien Elmes <anki@ichi2.net> # Copyright: Damien Elmes <anki@ichi2.net>
# License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html # License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html
DECK_VERSION = 72 DECK_VERSION = 73
from anki.lang import _ from anki.lang import _
from anki.media import rebuildMediaDir from anki.media import rebuildMediaDir
@ -45,9 +45,6 @@ create index if not exists ix_cards_priority on cards
# card spacing # card spacing
deck.s.statement(""" deck.s.statement("""
create index if not exists ix_cards_factId on cards (factId)""") create index if not exists ix_cards_factId on cards (factId)""")
# stats
deck.s.statement("""
create index if not exists ix_stats_typeDay on stats (type, day)""")
# fields # fields
deck.s.statement(""" deck.s.statement("""
create index if not exists ix_fields_factId on fields (factId)""") create index if not exists ix_fields_factId on fields (factId)""")
@ -230,10 +227,18 @@ this message. (ERR-0101)""") % {
deck.version = 71 deck.version = 71
deck.s.commit() deck.s.commit()
if deck.version < 72: if deck.version < 72:
# remove the expensive value cache # this was only used for calculating average factor
deck.s.statement("drop index if exists ix_cards_factor") deck.s.statement("drop index if exists ix_cards_factor")
deck.version = 72 deck.version = 72
deck.s.commit() deck.s.commit()
if deck.version < 73:
# remove stats, as it's all in the revlog now
deck.s.statement("drop index if exists ix_stats_typeDay")
deck.s.statement("drop table if exists stats")
deck.version = 73
deck.s.commit()
# executing a pragma here is very slow on large decks, so we store # executing a pragma here is very slow on large decks, so we store
# our own record # our own record
if not deck.getInt("pageSize") == 4096: if not deck.getInt("pageSize") == 4096:

View file

@ -9,7 +9,6 @@ from anki.db import *
from anki.stdmodels import BasicModel from anki.stdmodels import BasicModel
from anki.sync import SyncClient, SyncServer, HttpSyncServer, HttpSyncServerProxy from anki.sync import SyncClient, SyncServer, HttpSyncServer, HttpSyncServerProxy
from anki.sync import copyLocalMedia from anki.sync import copyLocalMedia
from anki.stats import dailyStats, globalStats
from anki.facts import Fact from anki.facts import Fact
from anki.cards import Card from anki.cards import Card
from anki.models import FieldModel from anki.models import FieldModel
@ -104,8 +103,6 @@ def test_localsync_deck():
c = deck1.getCard() c = deck1.getCard()
deck1.answerCard(c, 4) deck1.answerCard(c, 4)
client.sync() client.sync()
assert dailyStats(deck2).reps == 1
assert globalStats(deck2).reps == 1
assert deck2.s.scalar("select count(*) from reviewHistory") == 1 assert deck2.s.scalar("select count(*) from reviewHistory") == 1
# make sure meta data is synced # make sure meta data is synced
deck1.setVar("foo", 1) deck1.setVar("foo", 1)