1
0
mirror of https://github.com/lana-k/sqliteviz.git synced 2025-12-06 18:18:53 +08:00

3 Commits

Author SHA1 Message Date
saaj
9c0103fd05 SQLite 3.41.0 and pearson correlation extension function (#106)
* Build SQLite 3.41.0

* Update pivot_vtab

* Add Pearson correlation coefficient function extension, build

* Add an easy way to running test locally without Nodejs

* Use RSS sum to pick top2 processes for the report

* Try previous Ubuntu LTS as a workaround for Firefox worker not starting
2023-03-04 22:51:25 +01:00
saaj
e4b117ffb9 Sqljs upgrade and benchmark improvements (#103)
* Update to sql.js 1.7.0

* Update to emsdk 3.0.1, replace/remove deprecated/irrelevant settings

- Renamed .bc extension to .o
- Remove deprecated INLINING_LIMIT setting
- Remove SINGLE_FILE

* Update SQLite to 3.39.3

* Collect and plot CPU and RSS charts from the benchmark containers

* Move procpath commands to a playbook, plot only top 2 RSS & CPU usage

* Optimise for size, put -flto for both compile and link
2023-03-04 17:00:46 +01:00
lana-k
3c456ef135 fix sql hint: read properties of undefined #99 2022-07-29 15:27:01 +02:00
18 changed files with 468 additions and 310 deletions

View File

@@ -11,7 +11,7 @@ on:
jobs: jobs:
test: test:
name: Run tests name: Run tests
runs-on: ubuntu-latest runs-on: ubuntu-20.04
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Use Node.js - name: Use Node.js

24
Dockerfile.test Normal file
View File

@@ -0,0 +1,24 @@
# An easy way to run tests locally without Nodejs installed:
#
# docker build -t sqliteviz/test -f Dockerfile.test .
#
FROM node:12
RUN set -ex; \
apt update; \
apt install -y chromium firefox-esr; \
npm install -g npm@7
WORKDIR /tmp/build
COPY package.json package-lock.json ./
COPY lib lib
RUN npm install
COPY . .
RUN set -ex; \
sed -i 's/browsers: \[.*\],/browsers: ['"'FirefoxHeadlessTouch'"'],/' karma.conf.js
RUN npm run lint -- --no-fix && npm run test

View File

@@ -1,4 +1,4 @@
FROM emscripten/emsdk:2.0.24 FROM emscripten/emsdk:3.0.1
WORKDIR /tmp/build WORKDIR /tmp/build

View File

@@ -43,6 +43,8 @@ SQLite [miscellaneous extensions][3] included:
SQLite 3rd party extensions included: SQLite 3rd party extensions included:
1. [pivot_vtab][5] -- a pivot virtual table 1. [pivot_vtab][5] -- a pivot virtual table
2. `pearson` correlation coefficient function extension from [sqlean][21]
(which is part of [squib][20])
To ease the step to have working clone locally, the build is committed into To ease the step to have working clone locally, the build is committed into
the repository. the repository.
@@ -99,3 +101,5 @@ described in [this message from SQLite Forum][12]:
[17]: https://sqlite.org/contrib/ [17]: https://sqlite.org/contrib/
[18]: https://sqlite.org/contrib//download/extension-functions.c?get=25 [18]: https://sqlite.org/contrib//download/extension-functions.c?get=25
[19]: https://github.com/lana-k/sqliteviz/blob/master/tests/lib/database/sqliteExtensions.spec.js [19]: https://github.com/lana-k/sqliteviz/blob/master/tests/lib/database/sqliteExtensions.spec.js
[20]: https://github.com/mrwilson/squib/blob/master/pearson.c
[21]: https://github.com/nalgeon/sqlean/blob/incubator/src/pearson.c

View File

@@ -1,14 +1,25 @@
# SQLite WebAssembly build micro-benchmark # SQLite WebAssembly build micro-benchmark
This directory contains a micro-benchmark for evaluating SQLite This directory contains a micro-benchmark for evaluating SQLite WebAssembly
WebAssembly builds performance on typical SQL queries, run from builds performance on read and write SQL queries, run from `make.sh` script. If
`make.sh` script. It can also serve as a smoke test. the script has permission to `nice` processes and [Procpath][1] is installed,
e.g. it is run with `sudo -E env PATH=$PATH ./make.sh`, it'll `renice` all
processes running inside the benchmark containers. It can also serve as a smoke
test (e.g. for memory leaks).
The benchmark operates on a set of SQLite WebAssembly builds expected The benchmark operates on a set of SQLite WebAssembly builds expected in
in `lib/build-$NAME` directories each containing `sql-wasm.js` and `lib/build-$NAME` directories each containing `sql-wasm.js` and
`sql-wasm.wasm`. Then it creates a Docker image for each, and runs `sql-wasm.wasm`. Then it creates a Docker image for each, and runs the
the benchmark in Firefox and Chromium using Karma in the container. benchmark in Firefox and Chromium using Karma in the container.
After successful run, the benchmark result of each build is contained After successful run, the benchmark produces the following per each build:
in `build-$NAME-result.json`. The JSON result files can be analysed
using `result-analysis.ipynb` Jupyter notebook. - `build-$NAME-result.json`
- `build-$NAME.sqlite` (if Procpath is installed)
- `build-$NAME.svg` (if Procpath is installed)
These files can be analysed using `result-analysis.ipynb` Jupyter notebook.
The SVG is a chart with CPU and RSS usage of each test container (i.e. Chromium
run, then Firefox run per container).
[1]: https://pypi.org/project/Procpath/

View File

@@ -1,7 +1,8 @@
#!/bin/bash -e #!/bin/bash -e
cleanup () { cleanup () {
rm -rf lib/dist $flag_file rm -rf lib/dist "$renice_flag_file"
docker rm -f sqljs-benchmark-run 2> /dev/null || true
} }
trap cleanup EXIT trap cleanup EXIT
@@ -11,34 +12,36 @@ if [ ! -f sample.csv ]; then
| gunzip -c > sample.csv | gunzip -c > sample.csv
fi fi
PLAYBOOK=procpath/karma_docker.procpath
# for renice to work run like "sudo -E env PATH=$PATH ./make.sh" # for renice to work run like "sudo -E env PATH=$PATH ./make.sh"
test_ni=$(nice -n -1 nice) test_ni=$(nice -n -5 nice)
if [ $test_ni == -1 ]; then if [ $test_ni == -5 ]; then
flag_file=$(mktemp) renice_flag_file=$(mktemp)
fi fi
( {
while [ -f $flag_file ]; do while [ -f $renice_flag_file ]; do
root_pid=$( procpath --logging-level ERROR play -f $PLAYBOOK renice:watch
docker ps -f status=running -f name='^sqljs-benchmark-' -q \ done
| xargs -r -I{} -- docker inspect -f '{{.State.Pid}}' {} } &
)
if [ ! -z $root_pid ]; then
procpath query -d $'\n' "$..children[?(@.stat.pid == $root_pid)]..pid" \
| xargs -I{} -- renice -n -1 -p {} > /dev/null
fi
sleep 1
done &
)
shopt -s nullglob shopt -s nullglob
for d in lib/build-* ; do for d in lib/build-* ; do
rm -rf lib/dist rm -rf lib/dist
cp -r $d lib/dist cp -r $d lib/dist
sample_name=$(basename $d)
name=$(basename $d) docker build -t sqliteviz/sqljs-benchmark .
docker build -t sqliteviz/sqljs-benchmark:$name . docker rm sqljs-benchmark-run 2> /dev/null || true
docker rm sqljs-benchmark-$name 2> /dev/null || true docker run -d -it --cpus 2 --name sqljs-benchmark-run sqliteviz/sqljs-benchmark
docker run -it --cpus 2 --name sqljs-benchmark-$name sqliteviz/sqljs-benchmark:$name {
docker cp sqljs-benchmark-$name:/tmp/build/suite-result.json ${name}-result.json rm -f ${sample_name}.sqlite
docker rm sqljs-benchmark-$name procpath play -f $PLAYBOOK -o database_file=${sample_name}.sqlite track:record
procpath play -f $PLAYBOOK -o database_file=${sample_name}.sqlite \
-o plot_file=${sample_name}.svg track:plot
} &
docker attach sqljs-benchmark-run
docker cp sqljs-benchmark-run:/tmp/build/suite-result.json ${sample_name}-result.json
docker rm sqljs-benchmark-run
done done

View File

@@ -0,0 +1,28 @@
# This command may run when "sqljs-benchmark-run" does not yet exist or run
[renice:watch]
interval: 2
repeat: 30
environment:
ROOT_PID=docker inspect -f "{{.State.Pid}}" sqljs-benchmark-run 2> /dev/null || true
query:
PIDS=$..children[?(@.stat.pid in [$ROOT_PID])]..pid
command:
echo $PIDS | tr , '\n' | xargs --no-run-if-empty -I{} -- renice -n -5 -p {}
# Expected input arguments: database_file
[track:record]
interval: 1
stop_without_result: 1
environment:
ROOT_PID=docker inspect -f "{{.State.Pid}}" sqljs-benchmark-run
query:
$..children[?(@.stat.pid == $ROOT_PID)]
pid_list: $ROOT_PID
# Expected input arguments: database_file, plot_file
[track:plot]
moving_average_window: 5
title: Chromium vs Firefox (№1 RSS, №2 CPU)
custom_query_file:
procpath/top2_rss.sql
procpath/top2_cpu.sql

View File

@@ -0,0 +1,29 @@
WITH diff_all AS (
SELECT
record_id,
ts,
stat_pid,
stat_utime + stat_stime - LAG(stat_utime + stat_stime) OVER (
PARTITION BY stat_pid
ORDER BY record_id
) tick_diff,
ts - LAG(ts) OVER (
PARTITION BY stat_pid
ORDER BY record_id
) ts_diff
FROM record
), diff AS (
SELECT * FROM diff_all WHERE tick_diff IS NOT NULL
), one_time_pid_condition AS (
SELECT stat_pid
FROM record
GROUP BY 1
ORDER BY SUM(stat_utime + stat_stime) DESC
LIMIT 2
)
SELECT
ts,
stat_pid pid,
100.0 * tick_diff / (SELECT value FROM meta WHERE key = 'clock_ticks') / ts_diff value
FROM diff
JOIN one_time_pid_condition USING(stat_pid)

View File

@@ -0,0 +1,13 @@
WITH one_time_pid_condition AS (
SELECT stat_pid
FROM record
GROUP BY 1
ORDER BY SUM(stat_rss) DESC
LIMIT 2
)
SELECT
ts,
stat_pid pid,
stat_rss / 1024.0 / 1024 * (SELECT value FROM meta WHERE key = 'page_size') value
FROM record
JOIN one_time_pid_condition USING(stat_pid)

File diff suppressed because one or more lines are too long

View File

@@ -2,9 +2,11 @@ import logging
import subprocess import subprocess
from pathlib import Path from pathlib import Path
# See the setting descriptions on these pages:
# - https://emscripten.org/docs/optimizing/Optimizing-Code.html
# - https://github.com/emscripten-core/emscripten/blob/main/src/settings.js
cflags = ( cflags = (
'-O2', # SQLite configuration
'-DSQLITE_DEFAULT_CACHE_SIZE=-65536', # 64 MiB '-DSQLITE_DEFAULT_CACHE_SIZE=-65536', # 64 MiB
'-DSQLITE_DEFAULT_MEMSTATUS=0', '-DSQLITE_DEFAULT_MEMSTATUS=0',
'-DSQLITE_DEFAULT_SYNCHRONOUS=0', '-DSQLITE_DEFAULT_SYNCHRONOUS=0',
@@ -13,26 +15,26 @@ cflags = (
'-DSQLITE_ENABLE_FTS3', '-DSQLITE_ENABLE_FTS3',
'-DSQLITE_ENABLE_FTS3_PARENTHESIS', '-DSQLITE_ENABLE_FTS3_PARENTHESIS',
'-DSQLITE_ENABLE_FTS5', '-DSQLITE_ENABLE_FTS5',
'-DSQLITE_ENABLE_JSON1',
'-DSQLITE_ENABLE_NORMALIZE', '-DSQLITE_ENABLE_NORMALIZE',
'-DSQLITE_EXTRA_INIT=extra_init', '-DSQLITE_EXTRA_INIT=extra_init',
'-DSQLITE_OMIT_DEPRECATED', '-DSQLITE_OMIT_DEPRECATED',
'-DSQLITE_OMIT_LOAD_EXTENSION', '-DSQLITE_OMIT_LOAD_EXTENSION',
'-DSQLITE_OMIT_SHARED_CACHE', '-DSQLITE_OMIT_SHARED_CACHE',
'-DSQLITE_THREADSAFE=0', '-DSQLITE_THREADSAFE=0',
# Compile-time optimisation
'-Os', # reduces the code size about in half comparing to -O2
'-flto',
) )
emflags = ( emflags = (
# Base # Base
'--memory-init-file', '0', '--memory-init-file', '0',
'-s', 'RESERVED_FUNCTION_POINTERS=64',
'-s', 'ALLOW_TABLE_GROWTH=1', '-s', 'ALLOW_TABLE_GROWTH=1',
'-s', 'SINGLE_FILE=0',
# WASM # WASM
'-s', 'WASM=1', '-s', 'WASM=1',
'-s', 'ALLOW_MEMORY_GROWTH=1', '-s', 'ALLOW_MEMORY_GROWTH=1',
# Optimisation '-s', 'ENVIRONMENT=web,worker',
'-s', 'INLINING_LIMIT=50', # Link-time optimisation
'-O3', '-Os',
'-flto', '-flto',
# sql.js # sql.js
'-s', 'EXPORTED_FUNCTIONS=@src/sqljs/exported_functions.json', '-s', 'EXPORTED_FUNCTIONS=@src/sqljs/exported_functions.json',
@@ -50,22 +52,22 @@ def build(src: Path, dst: Path):
'emcc', 'emcc',
*cflags, *cflags,
'-c', src / 'sqlite3.c', '-c', src / 'sqlite3.c',
'-o', out / 'sqlite3.bc', '-o', out / 'sqlite3.o',
]) ])
logging.info('Building LLVM bitcode for extension-functions.c') logging.info('Building LLVM bitcode for extension-functions.c')
subprocess.check_call([ subprocess.check_call([
'emcc', 'emcc',
*cflags, *cflags,
'-c', src / 'extension-functions.c', '-c', src / 'extension-functions.c',
'-o', out / 'extension-functions.bc', '-o', out / 'extension-functions.o',
]) ])
logging.info('Building WASM from bitcode') logging.info('Building WASM from bitcode')
subprocess.check_call([ subprocess.check_call([
'emcc', 'emcc',
*emflags, *emflags,
out / 'sqlite3.bc', out / 'sqlite3.o',
out / 'extension-functions.bc', out / 'extension-functions.o',
'-o', out / 'sql-wasm.js', '-o', out / 'sql-wasm.js',
]) ])

View File

@@ -8,7 +8,7 @@ from pathlib import Path
from urllib import request from urllib import request
amalgamation_url = 'https://sqlite.org/2022/sqlite-amalgamation-3390000.zip' amalgamation_url = 'https://sqlite.org/2023/sqlite-amalgamation-3410000.zip'
# Extension-functions # Extension-functions
# =================== # ===================
@@ -28,10 +28,11 @@ extension_urls = (
('https://sqlite.org/src/raw/09f967dc?at=decimal.c', 'sqlite3_decimal_init'), ('https://sqlite.org/src/raw/09f967dc?at=decimal.c', 'sqlite3_decimal_init'),
# Third-party extension # Third-party extension
# ===================== # =====================
('https://github.com/jakethaw/pivot_vtab/raw/08ab0797/pivot_vtab.c', 'sqlite3_pivotvtab_init'), ('https://github.com/jakethaw/pivot_vtab/raw/9323ef93/pivot_vtab.c', 'sqlite3_pivotvtab_init'),
('https://github.com/nalgeon/sqlean/raw/95e8d21a/src/pearson.c', 'sqlite3_pearson_init'),
) )
sqljs_url = 'https://github.com/sql-js/sql.js/archive/refs/tags/v1.5.0.zip' sqljs_url = 'https://github.com/sql-js/sql.js/archive/refs/tags/v1.7.0.zip'
def _generate_extra_init_c_function(init_function_names): def _generate_extra_init_c_function(init_function_names):

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@@ -1,6 +1,6 @@
{ {
"name": "sqliteviz", "name": "sqliteviz",
"version": "0.21.0", "version": "0.21.1",
"license": "Apache-2.0", "license": "Apache-2.0",
"private": true, "private": true,
"scripts": { "scripts": {

View File

@@ -3,14 +3,20 @@ import 'codemirror/addon/hint/show-hint.js'
import 'codemirror/addon/hint/sql-hint.js' import 'codemirror/addon/hint/sql-hint.js'
import store from '@/store' import store from '@/store'
function _getHintText (hint) {
return typeof hint === 'string' ? hint : hint.text
}
export function getHints (cm, options) { export function getHints (cm, options) {
const token = cm.getTokenAt(cm.getCursor()).string.toUpperCase()
const result = CM.hint.sql(cm, options) const result = CM.hint.sql(cm, options)
// Don't show the hint if there is only one option // Don't show the hint if there is only one option
// and the token is already completed with this option // and the replacingText is already equals to this option
if (result.list.length === 1 && result.list[0].text.toUpperCase() === token) { const replacedText = cm.getRange(result.from, result.to).toUpperCase()
if (result.list.length === 1 &&
_getHintText(result.list[0]).toUpperCase() === replacedText) {
result.list = [] result.list = []
} }
return result return result
} }

View File

@@ -160,39 +160,39 @@ describe('SQLite extensions', function () {
it('supports transitive_closure', async function () { it('supports transitive_closure', async function () {
const actual = await db.execute(` const actual = await db.execute(`
CREATE TABLE node( CREATE TABLE node(
node_id INTEGER NOT NULL PRIMARY KEY, node_id INTEGER NOT NULL PRIMARY KEY,
parent_id INTEGER, parent_id INTEGER,
name VARCHAR(127), name VARCHAR(127),
FOREIGN KEY (parent_id) REFERENCES node(node_id) FOREIGN KEY (parent_id) REFERENCES node(node_id)
);
CREATE INDEX node_parent_id_idx ON node(parent_id);
CREATE VIRTUAL TABLE node_closure USING transitive_closure(
tablename = "node",
idcolumn = "node_id",
parentcolumn = "parent_id"
); );
CREATE INDEX node_parent_id_idx ON node(parent_id);
INSERT INTO node VALUES CREATE VIRTUAL TABLE node_closure USING transitive_closure(
(1, NULL, 'tests'), tablename = "node",
(2, 1, 'lib'), idcolumn = "node_id",
(3, 2, 'database'), parentcolumn = "parent_id"
(4, 2, 'utils'), );
(5, 2, 'storedQueries.spec.js'),
(6, 3, '_sql.spec.js'),
(7, 3, '_statements.spec.js'),
(8, 3, 'database.spec.js'),
(9, 3, 'sqliteExtensions.spec.js'),
(10, 4, 'fileIo.spec.js'),
(11, 4, 'time.spec.js');
SELECT name INSERT INTO node VALUES
FROM node (1, NULL, 'tests'),
WHERE node_id IN ( (2, 1, 'lib'),
SELECT nc.id (3, 2, 'database'),
FROM node_closure AS nc (4, 2, 'utils'),
WHERE nc.root = 2 AND nc.depth = 2 (5, 2, 'storedQueries.spec.js'),
); (6, 3, '_sql.spec.js'),
(7, 3, '_statements.spec.js'),
(8, 3, 'database.spec.js'),
(9, 3, 'sqliteExtensions.spec.js'),
(10, 4, 'fileIo.spec.js'),
(11, 4, 'time.spec.js');
SELECT name
FROM node
WHERE node_id IN (
SELECT nc.id
FROM node_closure AS nc
WHERE nc.root = 2 AND nc.depth = 2
);
`) `)
expect(actual.values).to.eql({ expect(actual.values).to.eql({
name: [ name: [
@@ -293,7 +293,7 @@ describe('SQLite extensions', function () {
it('supports decimal', async function () { it('supports decimal', async function () {
const actual = await db.execute(` const actual = await db.execute(`
select SELECT
decimal_add(decimal('0.1'), decimal('0.2')) "add", decimal_add(decimal('0.1'), decimal('0.2')) "add",
decimal_sub(0.2, 0.1) sub, decimal_sub(0.2, 0.1) sub,
decimal_mul(power(2, 69), 2) mul, decimal_mul(power(2, 69), 2) mul,
@@ -430,4 +430,29 @@ describe('SQLite extensions', function () {
] ]
}) })
}) })
it('supports pearson', async function () {
const actual = await db.execute(`
CREATE TABLE dataset(x REAL, y REAL, z REAL);
INSERT INTO dataset VALUES
(5,3,3.2), (5,6,4.3), (5,9,5.4),
(10,3,4), (10,6,3.8), (10,9,3.6),
(15,3,4.8), (15,6,4), (15,9,3.5);
SELECT
pearson(x, x) xx,
pearson(x, y) xy,
abs(-0.12666 - pearson(x, z)) < 0.00001 xz,
pearson(y, x) yx,
pearson(y, y) yy,
abs(0.10555 - pearson(y, z)) < 0.00001 yz,
abs(-0.12666 - pearson(z, x)) < 0.00001 zx,
abs(0.10555 - pearson(z, y)) < 0.00001 zy,
pearson(z, z) zz
FROM dataset;
`)
expect(actual.values).to.eql({
xx: [1], xy: [0], xz: [1], yx: [0], yy: [1], yz: [1], zx: [1], zy: [1], zz: [1]
})
})
}) })

View File

@@ -138,15 +138,37 @@ describe('hint.js', () => {
'getHints returns [ ] if there is only one option and token is completed with this option', 'getHints returns [ ] if there is only one option and token is completed with this option',
() => { () => {
// mock CM.hint.sql and editor // mock CM.hint.sql and editor
sinon.stub(CM.hint, 'sql').returns({ list: [{ text: 'SELECT' }] }) sinon.stub(CM.hint, 'sql').returns({
list: [{ text: 'SELECT' }],
from: null, // from/to doesn't metter because getRange is mocked
to: null
})
const editor = { const editor = {
getTokenAt () { getRange () {
return { return 'select'
string: 'select', }
type: 'keyword' }
}
}, const hints = getHints(editor, {})
getCursor: sinon.stub() expect(hints.list).to.eql([])
}
)
it(
'getHints returns [ ] if there is only one string option and token ' +
'is completed with this option',
() => {
// mock CM.hint.sql and editor
sinon.stub(CM.hint, 'sql').returns({
list: ['house.name'],
from: null, // from/to doesn't metter because getRange is mocked
to: null
})
const editor = {
getRange () {
return 'house.name'
}
} }
const hints = getHints(editor, {}) const hints = getHints(editor, {})
@@ -160,15 +182,11 @@ describe('hint.js', () => {
{ text: 'SELECT' }, { text: 'SELECT' },
{ text: 'ST' } { text: 'ST' }
] ]
sinon.stub(CM.hint, 'sql').returns({ list }) sinon.stub(CM.hint, 'sql').returns({ list, from: null, to: null })
const editor = { const editor = {
getTokenAt () { getRange () {
return { return 'se'
string: 'se', }
type: 'keyword'
}
},
getCursor: sinon.stub()
} }
const hints = getHints(editor, {}) const hints = getHints(editor, {})
@@ -182,15 +200,11 @@ describe('hint.js', () => {
() => { () => {
// mock CM.hint.sql and editor // mock CM.hint.sql and editor
const list = [{ text: 'SELECT' }] const list = [{ text: 'SELECT' }]
sinon.stub(CM.hint, 'sql').returns({ list }) sinon.stub(CM.hint, 'sql').returns({ list, from: null, to: null })
const editor = { const editor = {
getTokenAt () { getRange () {
return { return 'sele'
string: 'sele', }
type: 'keyword'
}
},
getCursor: sinon.stub()
} }
const hints = getHints(editor, {}) const hints = getHints(editor, {})