Skip to content

Commit e011dce

Browse files
committed
Database optimizations to speed up web statistics.
1 parent 352493d commit e011dce

File tree

6 files changed

+47
-39
lines changed

6 files changed

+47
-39
lines changed

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ Three components:
88
* listener - logs mouse and keyboard input, can run individually
99
* webui - web frontend for statistics and heatmaps, can run individually
1010

11-
Listener and web-UI components can be run separately, or launched from main.
11+
Listener and web-UI components can be run separately.
1212

1313
Data is kept in an SQLite database, under inputscope/var.
1414

inputscope/conf.py

+22-9
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
2121
@author Erki Suurjaak
2222
@created 26.03.2015
23-
@modified 06.05.2015
23+
@modified 19.05.2015
2424
------------------------------------------------------------------------------
2525
"""
2626
try: import ConfigParser as configparser # Py2
@@ -36,8 +36,8 @@
3636

3737
"""Program title, version number and version date."""
3838
Title = "InputScope"
39-
Version = "1.0"
40-
VersionDate = "06.05.2015"
39+
Version = "1.1a"
40+
VersionDate = "19.05.2015"
4141

4242
"""TCP port of the web user interface."""
4343
WebHost = "localhost"
@@ -222,16 +222,29 @@
222222
"""Path for application icon file."""
223223
IconPath = os.path.join(StaticPath, "icon.ico")
224224

225+
"""SQL template for trigger to update day counts."""
226+
TriggerTemplate = """
227+
CREATE TRIGGER IF NOT EXISTS on_insert_{0} AFTER INSERT ON {0}
228+
BEGIN
229+
INSERT OR IGNORE INTO counts (type, day, count) VALUES ('{0}', NEW.day, 0);
230+
UPDATE counts SET count = count + 1 WHERE type = '{0}' AND day = NEW.day;
231+
END;"""
232+
233+
"""SQL template for day field index."""
234+
DayIndexTemplate = "CREATE INDEX IF NOT EXISTS idx_{0}_day ON {0} (day)"
235+
225236
"""Statements to execute in database at startup, like CREATE TABLE."""
226237
DbStatements = (
227-
"CREATE TABLE IF NOT EXISTS moves (id INTEGER NOT NULL PRIMARY KEY, dt TIMESTAMP, x INTEGER, y INTEGER)",
228-
"CREATE TABLE IF NOT EXISTS clicks (id INTEGER NOT NULL PRIMARY KEY, dt TIMESTAMP, x INTEGER, y INTEGER, button INTEGER)",
229-
"CREATE TABLE IF NOT EXISTS scrolls (id INTEGER NOT NULL PRIMARY KEY, dt TIMESTAMP, x INTEGER, y INTEGER, wheel INTEGER)",
230-
"CREATE TABLE IF NOT EXISTS keys (id INTEGER NOT NULL PRIMARY KEY, dt TIMESTAMP, key TEXT, realkey TEXT)",
231-
"CREATE TABLE IF NOT EXISTS combos (id INTEGER NOT NULL PRIMARY KEY, dt TIMESTAMP, key TEXT, realkey TEXT)",
238+
"CREATE TABLE IF NOT EXISTS moves (id INTEGER NOT NULL PRIMARY KEY, day DATE, stamp REAL, x INTEGER, y INTEGER)",
239+
"CREATE TABLE IF NOT EXISTS clicks (id INTEGER NOT NULL PRIMARY KEY, day DATE, stamp REAL, x INTEGER, y INTEGER, button INTEGER)",
240+
"CREATE TABLE IF NOT EXISTS scrolls (id INTEGER NOT NULL PRIMARY KEY, day DATE, stamp REAL, x INTEGER, y INTEGER, wheel INTEGER)",
241+
"CREATE TABLE IF NOT EXISTS keys (id INTEGER NOT NULL PRIMARY KEY, day DATE, stamp REAL, key TEXT, realkey TEXT)",
242+
"CREATE TABLE IF NOT EXISTS combos (id INTEGER NOT NULL PRIMARY KEY, day DATE, stamp REAL, key TEXT, realkey TEXT)",
232243
"CREATE TABLE IF NOT EXISTS app_events (id INTEGER NOT NULL PRIMARY KEY, dt TIMESTAMP DEFAULT (DATETIME('now', 'localtime')), type TEXT)",
233244
"CREATE TABLE IF NOT EXISTS screen_sizes (id INTEGER NOT NULL PRIMARY KEY, dt TIMESTAMP DEFAULT (DATETIME('now', 'localtime')), x INTEGER, y INTEGER)",
234-
)
245+
"CREATE TABLE IF NOT EXISTS counts (id INTEGER NOT NULL PRIMARY KEY, type TEXT, day DATETIME, count INTEGER, UNIQUE(type, day))",
246+
) + tuple(TriggerTemplate.format(x) for x in ["moves", "clicks", "scrolls", "keys", "combos"]
247+
) + tuple(DayIndexTemplate.format(x) for x in ["moves", "clicks", "scrolls", "keys", "combos"])
235248

236249

237250
def init(filename=ConfigPath):

inputscope/listener.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,14 @@
66
77
@author Erki Suurjaak
88
@created 06.04.2015
9-
@modified 07.05.2015
9+
@modified 19.05.2015
1010
"""
1111
from __future__ import print_function
1212
import datetime
1313
import Queue
1414
import sys
1515
import threading
16+
import time
1617
import pykeyboard
1718
import pymouse
1819

@@ -102,7 +103,7 @@ def stop(self):
102103
db.close()
103104

104105
def handle(self, **kwargs):
105-
kwargs["dt"] = datetime.datetime.now()
106+
kwargs.update(day=datetime.date.today(), stamp=time.time())
106107
self.inqueue.put(kwargs)
107108

108109

inputscope/main.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
66
@author Erki Suurjaak
77
@created 05.05.2015
8-
@modified 04.05.2015
8+
@modified 19.05.2015
99
"""
1010
import multiprocessing
1111
import multiprocessing.forking
@@ -122,7 +122,7 @@ def OnInit(self):
122122
self.trayicon.Bind(wx.EVT_TASKBAR_RIGHT_DOWN, self.OnOpenMenu)
123123
self.frame_console.Bind(wx.EVT_CLOSE, self.OnToggleConsole)
124124

125-
self.model.log_resolution(wx.GetDisplaySize())
125+
wx.CallAfter(self.model.log_resolution, wx.GetDisplaySize())
126126
wx.CallAfter(self.model.start)
127127
return True # App.OnInit returns whether processing should continue
128128

inputscope/var/inputscope.db

9 KB
Binary file not shown.

inputscope/webui.py

+19-25
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
77
@author Erki Suurjaak
88
@created 06.04.2015
9-
@modified 06.05.2015
9+
@modified 18.05.2015
1010
"""
1111
import collections
1212
import datetime
@@ -39,8 +39,9 @@ def server_static(filepath):
3939
@route("/mouse/<table>/<day>")
4040
def mouse(table, day=None):
4141
"""Handler for showing mouse statistics for specified type and day."""
42-
where = (("DATE(dt)", day),) if day else ()
43-
events = db.fetch(table, where=where, order="dt")
42+
where = (("day", day),) if day else ()
43+
events = db.fetch(table, where=where, order="day")
44+
for e in events: e["dt"] = datetime.datetime.fromtimestamp(e["stamp"])
4445
stats, positions, events = stats_mouse(events, table)
4546
return bottle.template("mouse.tpl", locals(), conf=conf)
4647

@@ -50,12 +51,13 @@ def mouse(table, day=None):
5051
def keyboard(table, day=None):
5152
"""Handler for showing the keyboard statistics page."""
5253
cols, group = "realkey AS key, COUNT(*) AS count", "realkey"
53-
where = (("DATE(dt)", day),) if day else ()
54+
where = (("day", day),) if day else ()
5455
counts_display = counts = db.fetch(table, cols, where, group, "count DESC")
5556
if "combos" == table:
5657
counts_display = db.fetch(table, "key, COUNT(*) AS count", where,
5758
"key", "count DESC")
58-
events = db.fetch(table, where=where, order="id")
59+
events = db.fetch(table, where=where, order="stamp")
60+
for e in events: e["dt"] = datetime.datetime.fromtimestamp(e["stamp"])
5961
stats, collatedevents = stats_keyboard(events, table)
6062
return bottle.template("keyboard.tpl", locals(), conf=conf)
6163

@@ -64,23 +66,22 @@ def keyboard(table, day=None):
6466
def inputindex(input):
6567
"""Handler for showing keyboard or mouse page with day and total links."""
6668
stats = {}
67-
countminmax = "COUNT(*) AS count, MIN(DATE(dt)) AS first, MAX(DATE(dt)) AS last"
68-
dtcols = "DATE(dt) AS day, COUNT(*) AS count"
69+
countminmax = "SUM(count) AS count, MIN(day) AS first, MAX(day) AS last"
6970
tables = ("moves", "clicks", "scrolls") if "mouse" == input else ("keys", "combos")
7071
for table in tables:
71-
stats[table] = db.fetchone(table, countminmax)
72-
stats[table]["days"] = db.fetch(table, dtcols, group="day", order="day")
72+
stats[table] = db.fetchone("counts", countminmax, type=table)
73+
stats[table]["days"] = db.fetch("counts", order="day DESC", type=table)
7374
return bottle.template("input.tpl", locals(), conf=conf)
7475

7576

7677
@route("/")
7778
def index():
7879
"""Handler for showing the GUI index page."""
7980
stats = {"mouse": {"count": 0}, "keyboard": {"count": 0}}
80-
countminmax = "COUNT(*) AS count, MIN(DATE(dt)) AS first, MAX(DATE(dt)) AS last"
81+
countminmax = "SUM(count) AS count, MIN(day) AS first, MAX(day) AS last"
8182
tables = {"keyboard": ("keys", "combos"), "mouse": ("moves", "clicks", "scrolls")}
8283
for input, table in [(x, t) for x, tt in tables.items() for t in tt]:
83-
row = db.fetchone(table, countminmax)
84+
row = db.fetchone("counts", countminmax, type=table)
8485
if not row["count"]: continue # for input, table
8586
stats[input]["count"] += row["count"]
8687
for func, key in [(min, "first"), (max, "last")]:
@@ -120,7 +121,7 @@ def stats_keyboard(events, table):
120121
sum(deltas, datetime.timedelta()) / len(deltas)),
121122
] if "combos" == table else [
122123
("Keys per hour",
123-
int(len(events) / (timedelta_seconds(events[-1]["dt"] - events[0]["dt"]) / 3600))),
124+
int(3600 * len(events) / timedelta_seconds(events[-1]["dt"] - events[0]["dt"]))),
124125
("Average interval between keys",
125126
sum(deltas, datetime.timedelta()) / len(deltas)),
126127
("Typing sessions (key interval < %ss)" % UNBROKEN_DELTA.seconds,
@@ -176,40 +177,33 @@ def stats_mouse(events, table):
176177
(distance * conf.PixelLength, conf.PixelLength * 1000)),
177178
("Average speed", "%.1f pixels per second" % (distance / (seconds or 1))),
178179
("", "%.4f meters per second" %
179-
(distance * conf.PixelLength / (seconds or 1))),
180-
]
180+
(distance * conf.PixelLength / (seconds or 1))), ]
181181
elif "scrolls" == table:
182182
counts = collections.Counter(e["wheel"] for e in events)
183183
stats = [("Scrolls per hour",
184184
int(len(events) / (timedelta_seconds(events[-1]["dt"] - events[0]["dt"]) / 3600 or 1))),
185185
("Average interval", sum(deltas, datetime.timedelta()) / (len(deltas) or 1)),
186186
("Scrolls down", counts[-1]),
187-
("Scrolls up", counts[1]),
188-
]
187+
("Scrolls up", counts[1]), ]
189188
elif "clicks" == table:
190-
"average distance between clicks"
191189
counts = collections.Counter(e["button"] for e in events)
192190
NAMES = {1: "Left", 2: "Right", 3: "Middle"}
193191
stats = [("Clicks per hour",
194192
int(len(events) / (timedelta_seconds(events[-1]["dt"] - events[0]["dt"]) / 3600 or 1))),
195193
("Average interval between clicks",
196194
sum(deltas, datetime.timedelta()) / (len(deltas) or 1)),
197195
("Average distance between clicks",
198-
"%.1f pixels" % (distance / (len(events) or 1))),
199-
]
196+
"%.1f pixels" % (distance / (len(events) or 1))), ]
200197
for k, v in sorted(counts.items()):
201198
stats += [("%s button clicks" % NAMES.get(k, "%s." % k), v)]
202199
return stats, positions, events
203200

204201

205202
def timedelta_seconds(timedelta):
206203
"""Returns the total timedelta duration in seconds."""
207-
if hasattr(timedelta, "total_seconds"):
208-
result = timedelta.total_seconds()
209-
else: # Python 2.6 compatibility
210-
result = timedelta.days * 24 * 3600 + timedelta.seconds + \
211-
timedelta.microseconds / 1000000.
212-
return result
204+
return (timedelta.total_seconds() if hasattr(timedelta, "total_seconds")
205+
else timedelta.days * 24 * 3600 + timedelta.seconds +
206+
timedelta.microseconds / 1000000.)
213207

214208

215209
def init():

0 commit comments

Comments
 (0)