Skip to content

Commit 5dc54de

Browse files
authored
SQLite Codex Optimization (#963)
* SQLite Codex Optimization * KILL KILL KILL KILL KILL KILL KILL KILL KILL KILL * Log if we skipped Codex DB Generation * Code Review
1 parent 697ae21 commit 5dc54de

File tree

4 files changed

+217
-9
lines changed

4 files changed

+217
-9
lines changed

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,3 +207,8 @@ libprof.so
207207

208208
# Screenshot tests
209209
/artifacts
210+
211+
# Codex Database
212+
codex.db
213+
codex.db-shm
214+
codex.db-wal

code/__DEFINES/vv.dm

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@
143143

144144
// misc
145145
#define VV_HK_SPACEVINE_PURGE "spacevine_purge"
146+
#define VV_HK_REGENERATE_CODEX "purge_codex_db"
146147

147148
// paintings
148149
#define VV_HK_REMOVE_PAINTING "remove_painting"

code/_compile_options.dm

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,9 @@
106106
/// Uncomment this to enable debugging tools for map making.
107107
//#define DEBUG_MAPS
108108

109+
/// Force codex SQLite generation and loading despite being a debug server.
110+
//#define FORCE_CODEX_DATABASE 1
111+
109112
/////////////////////// REFERENCE TRACKING
110113

111114
///Used to find the sources of harddels, quite laggy, don't be surpised if it freezes your client for a good while
@@ -192,6 +195,10 @@
192195
#define TESTING
193196
#endif
194197

198+
#ifndef FORCE_CODEX_DATABASE
199+
#define FORCE_CODEX_DATABASE 0
200+
#endif
201+
195202
#ifdef UNIT_TESTS
196203
// Hard del testing defines
197204
#define REFERENCE_TRACKING

code/controllers/subsystem/codex.dm

Lines changed: 204 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,34 @@ SUBSYSTEM_DEF(codex)
77
var/regex/trailingLinebreakRegexStart
88
var/regex/trailingLinebreakRegexEnd
99

10+
/// All entries. Unkeyed.
1011
var/list/all_entries = list()
12+
/// All STATIC entries, By path. Does not include dynamic entries.
1113
var/list/entries_by_path = list()
14+
/// All entries, by name.
1215
var/list/entries_by_string = list()
16+
/// The same as above, but sorted (?)
1317
var/list/index_file = list()
18+
/// Search result cache, so we don't need to hit the DB every time.
1419
var/list/search_cache = list()
20+
/// All categories.
1521
var/list/codex_categories = list()
1622

23+
/// Codex Database Connection
24+
var/database/codex_index
25+
26+
/datum/controller/subsystem/codex/vv_get_dropdown()
27+
. = ..()
28+
VV_DROPDOWN_OPTION("", "---")
29+
VV_DROPDOWN_OPTION(VV_HK_REGENERATE_CODEX, "Regenerate Search Database")
30+
31+
/datum/controller/subsystem/codex/vv_do_topic(href_list)
32+
. = ..()
33+
if(href_list[VV_HK_REGENERATE_CODEX])
34+
if(tgui_alert(usr, "Are you sure you want to regenerate the search index? This will almost certainly cause lag.", "Regenerate Index", list("Yes", "No")) == "Yes")
35+
prepare_search_database(TRUE)
36+
37+
1738
/datum/controller/subsystem/codex/Initialize()
1839
// Codex link syntax is such:
1940
// <l>keyword</l> when keyword is mentioned verbatim,
@@ -44,6 +65,9 @@ SUBSYSTEM_DEF(codex)
4465
for(var/datum/codex_entry/entry as anything in all_entries)
4566
index_file[entry.name] = entry
4667
index_file = sortTim(index_file, GLOBAL_PROC_REF(cmp_text_asc))
68+
69+
// Prepare the search database.
70+
prepare_search_database()
4771
. = ..()
4872

4973
/datum/controller/subsystem/codex/proc/parse_links(string, viewer)
@@ -99,7 +123,7 @@ SUBSYSTEM_DEF(codex)
99123
codex_data += "<h3><b>[entries.len] matches</b>[search_query ? " for '[search_query]'" : ""]:</h3>"
100124

101125
if(LAZYLEN(entries) > CODEX_ENTRY_LIMIT)
102-
codex_data += "Showing first <b>[CODEX_ENTRY_LIMIT]</b> entries. <b>[entries.len - 5] result\s</b> omitted.</br>"
126+
codex_data += "Showing first <b>[CODEX_ENTRY_LIMIT]</b> entries. <b>[entries.len - CODEX_ENTRY_LIMIT] result\s</b> omitted.</br>"
103127
codex_data += "<table width = 100%>"
104128

105129
for(var/i = 1 to min(entries.len, CODEX_ENTRY_LIMIT))
@@ -111,24 +135,73 @@ SUBSYSTEM_DEF(codex)
111135
popup.set_content(codex_data.Join())
112136
popup.open()
113137

114-
#undef CODEX_ENTRY_LIMIT
138+
115139
/datum/controller/subsystem/codex/proc/get_guide(category)
116140
var/datum/codex_category/cat = codex_categories[category]
117141
. = cat?.guide_html
118142

143+
/// Perform a full-text search through all codex entries. Entries matching the query by name will be shown first.
144+
/// Results are cached. Relies on the index database.
119145
/datum/controller/subsystem/codex/proc/retrieve_entries_for_string(searching)
120146

121-
if(!initialized)
122-
return list()
123-
124147
searching = codex_sanitize(searching)
125-
126-
if(!searching)
127-
return list()
128-
129148
. = search_cache[searching]
130149
if(.)
131150
return .
151+
if(!searching || !initialized)
152+
return list()
153+
if(!codex_index) //No codex DB loaded. Use the fallback search.
154+
return text_search_no_db(searching)
155+
156+
157+
var/search_string = "%[searching]%"
158+
// Search by name to build the priority entries first
159+
var/database/query/cursor = new(
160+
{"SELECT name FROM codex_entries
161+
WHERE name LIKE ?
162+
ORDER BY name asc
163+
LIMIT [CODEX_ENTRY_LIMIT]"},
164+
search_string
165+
)
166+
// Execute the query, returning us a list of types we can retrieve from the list indexes.
167+
cursor.Execute(codex_index)
168+
169+
// God this sucks.
170+
var/list/datum/codex_entry/priority_results = list()
171+
while(cursor.NextRow())
172+
var/row = cursor.GetRowData()
173+
priority_results += index_file[row["name"]]
174+
CHECK_TICK
175+
176+
// Now the awful slow ones.
177+
cursor.Add(
178+
{"SELECT name FROM codex_entries
179+
WHERE lore_text LIKE ?
180+
AND mechanics_text LIKE ?
181+
AND antag_text LIKE ?
182+
ORDER BY name asc
183+
LIMIT [CODEX_ENTRY_LIMIT]"},
184+
search_string,
185+
search_string,
186+
search_string
187+
)
188+
// Execute the query, returning us a list of types we can retrieve from the list indexes.
189+
cursor.Execute(codex_index)
190+
var/list/datum/codex_entry/fulltext_results = list()
191+
while(cursor.NextRow())
192+
var/row = cursor.GetRowData()
193+
fulltext_results += index_file[row["name"]]
194+
CHECK_TICK
195+
196+
priority_results += fulltext_results
197+
. = search_cache[searching] = priority_results
198+
199+
/// Straight-DM implimentation of full text search. Objectively garbage.
200+
/// Does not use the DB. Used when database loading is skipped.
201+
/// Argument has already been sanitized.
202+
/// Safety checks have already been done. Cache has already been checked.
203+
/datum/controller/subsystem/codex/proc/text_search_no_db(searching)
204+
PRIVATE_PROC(TRUE)
132205

133206
var/list/results = list()
134207
var/list/priority_results = list()
@@ -168,3 +241,125 @@ SUBSYSTEM_DEF(codex)
168241
if(entry)
169242
present_codex_entry(showing_mob, entry)
170243
return TRUE
244+
245+
#define CODEX_SEARCH_INDEX_FILE "codex.db"
246+
#define CODEX_SERIAL_ALWAYS_VALID "I_DOWNLOADED_A_ZIP_INSTEAD_OF_USING_GIT"
247+
/// Prepare the search database.
248+
/datum/controller/subsystem/codex/proc/prepare_search_database(drop_existing = FALSE)
249+
if(GLOB.is_debug_server && !FORCE_CODEX_DATABASE)
250+
to_chat(world, span_debug("Codex: Debug server detected. DB operation disabled."))
251+
log_world("Codex: Codex DB generation Skipped")
252+
return
253+
if(drop_existing)
254+
to_chat(world, span_debug("Codex: Deleting old index..."))
255+
//Check if we've already opened one this round, if so, get rid of it.
256+
if(codex_index)
257+
del(codex_index)
258+
fdel(CODEX_SEARCH_INDEX_FILE)
259+
else
260+
to_chat(world, span_debug("Codex: Preparing Search Database"))
261+
262+
263+
if(!rustg_file_exists(CODEX_SEARCH_INDEX_FILE))
264+
if(!drop_existing)
265+
to_chat(world, span_debug("Codex: Database missing, building..."))
266+
create_db()
267+
build_db_index()
268+
269+
if(!codex_index) //If we didn't just create it, we need to load it.
270+
codex_index = new(CODEX_SEARCH_INDEX_FILE)
271+
272+
var/database/query/cursor = new("SELECT * FROM _info")
273+
if(!cursor.Execute(codex_index))
274+
to_chat(world, span_debug("Codex: ABORTING! Database error: [cursor.Error()] | [cursor.ErrorMsg()]"))
275+
return
276+
277+
cursor.NextRow()
278+
var/list/revline = cursor.GetRowData()
279+
var/db_serial = revline["revision"]
280+
if(db_serial != GLOB.revdata.commit)
281+
if(db_serial == CODEX_SERIAL_ALWAYS_VALID)
282+
to_chat(world, span_debug("Codex: Special Database Serial detected. Data may be inaccurate or out of date."))
283+
else
284+
to_chat(world, span_debug("Codex: Database out of date, Rebuilding..."))
285+
prepare_search_database(TRUE) //recursiveness funny,,
286+
return
287+
288+
if(drop_existing)
289+
to_chat(world, span_debug("Codex: Collation complete.\nCodex: Index ready."))
290+
return
291+
to_chat(world, span_debug("Codex: Database Serial validated.\nCodex: Loading complete."))
292+
293+
/datum/controller/subsystem/codex/proc/create_db()
294+
// No index? Make one.
295+
296+
to_chat(world, span_debug("Codex: Writing new database file..."))
297+
//We explicitly store the DB in the root directory, so that TGS builds wipe it.
298+
codex_index = new(CODEX_SEARCH_INDEX_FILE)
299+
300+
/// Holds the revision the index was compiled for. If it's different then live, we need to regenerate the index.
301+
var/static/create_info_schema = {"
302+
CREATE TABLE "_info" (
303+
"revision" TEXT
304+
);"}
305+
306+
//Create the initial schema
307+
var/database/query/init_cursor = new(create_info_schema)
308+
309+
if(!init_cursor.Execute(codex_index))
310+
to_chat(world, span_debug("Codex: ABORTING! Database error: [init_cursor.Error()] | [init_cursor.ErrorMsg()]"))
311+
return
312+
313+
// Holds all codex entries to enable accelerated text search.
314+
var/static/create_codex_schema = {"
315+
CREATE TABLE "codex_entries" (
316+
"name" TEXT NOT NULL,
317+
"lore_text" TEXT,
318+
"mechanics_text" TEXT,
319+
"antag_text" TEXT,
320+
PRIMARY KEY("name")
321+
);"}
322+
323+
init_cursor.Add(create_codex_schema)
324+
if(!init_cursor.Execute(codex_index))
325+
to_chat(world, span_debug("Codex: ABORTING! Database error: [init_cursor.Error()] | [init_cursor.ErrorMsg()]"))
326+
return
327+
328+
var/revid = GLOB.revdata.commit
329+
if(!revid) //zip download, you're on your own pissboy, The serial will always be considered valid.
330+
revid = CODEX_SERIAL_ALWAYS_VALID
331+
332+
//Insert the revision header.
333+
init_cursor.Add("INSERT INTO _info (revision) VALUES (?)", revid)
334+
if(!init_cursor.Execute(codex_index))
335+
to_chat(world, span_debug("Codex: ABORTING! Database error: [init_cursor.Error()] | [init_cursor.ErrorMsg()]"))
336+
return
337+
338+
/datum/controller/subsystem/codex/proc/build_db_index()
339+
to_chat(world, span_debug("Codex: Building search index."))
340+
341+
var/database/query/cursor = new
342+
var/total_entries = length(all_entries)
343+
to_chat(world, span_debug("\tCodex: Collating [total_entries] records..."))
344+
var/record_id = 0 //Counter for debugging.
345+
for(var/datum/codex_entry/entry as anything in all_entries)
346+
cursor.Add(
347+
"INSERT INTO codex_entries (name, lore_text, mechanics_text, antag_text) VALUES (?,?,?,?)",
348+
entry.name,
349+
entry.lore_text,
350+
entry.mechanics_text,
351+
entry.antag_text
352+
)
353+
354+
if(!cursor.Execute(codex_index))
355+
to_chat(world, span_debug("Codex: ABORTING! Database error: [cursor.Error()] | [cursor.ErrorMsg()]"))
356+
return
357+
358+
record_id++
359+
if((!(record_id % 100)) || (record_id == total_entries))
360+
to_chat(world, span_debug("\tCodex: [record_id]/[total_entries]..."))
361+
362+
CHECK_TICK //We'd deadlock the server otherwise.
363+
364+
#undef CODEX_SEARCH_INDEX_FILE
365+
#undef CODEX_ENTRY_LIMIT

0 commit comments

Comments
 (0)