Skip to content

Commit

Permalink
Merge pull request #63 from wongterrencew/improve_performance_with_le…
Browse files Browse the repository at this point in the history
…vel_db

Improve performance with level db
  • Loading branch information
terrencewwong authored Mar 23, 2017
2 parents ba5d6d7 + 35fdfde commit f0d2d78
Show file tree
Hide file tree
Showing 14 changed files with 771 additions and 683 deletions.
20 changes: 10 additions & 10 deletions lib/async-deck-loader/async-deck-loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,36 +3,32 @@ const path = require('path')
const storeDir = require('../store-dir-util').get()
const PouchDB = require('pouchdb')
PouchDB.plugin(require('pouchdb-upsert'))
const DB = PouchDB.defaults({
prefix: storeDir
})
const DB = require('../level-db-wrapper')

const LOADED_KEY = 'LOADED_KEY'

class AsyncDeckLoader {
name: string;
dbLocation: string;
storage: any; // TODO: make a Persistor interface

constructor (name: string, Persistor?: any) {
this.name = name
this.storage = Persistor ? new Persistor(this.name) : new DB(this.name)
this.dbLocation = path.join(storeDir, this.name)
this.storage = Persistor ? new Persistor(this.name) : new DB(this.dbLocation)
}

async isLoaded (): Promise<boolean> {
try {
const doc = await this.storage.get(LOADED_KEY)
return doc.loaded
return await this.storage.get(LOADED_KEY)
} catch (e) {
// TODO: maybe only return false for certain types of errors?
return false
}
}

async setLoaded (state: boolean): Promise<void> {
await this.storage.upsert(LOADED_KEY, doc => {
doc.loaded = state
return doc
})
return this.storage.put(LOADED_KEY, state)
}

load (args: {path?: string} = {}) {
Expand All @@ -54,6 +50,10 @@ class AsyncDeckLoader {
return undefined
}
}

getStorage () {
return this.storage
}
}

module.exports = AsyncDeckLoader
11 changes: 6 additions & 5 deletions lib/async-deck-loader/async-deck-loader.spec.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
const AsyncDeckLoader = require('./')
const expect = require('expect')
const path = require('path')

const Persistor = require('pouchdb')
Persistor.plugin(require('pouchdb-upsert'))
Persistor.plugin(require('pouchdb-adapter-memory'))
const InMemoryPersistor = Persistor.defaults({adapter: 'memory'})
const DB = require('../level-db-wrapper')
class InMemoryPersistor extends DB {
constructor (name) {
super(name, {adapter: 'memory'})
}
}

describe('async-deck-loader', () => {
describe('unit', () => {
Expand Down
31 changes: 16 additions & 15 deletions lib/learning-strategy/learning-strategy.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/* @flow */
const PouchQueue = require('../pouch-queue')
const LevelQueue = require('../level-queue')
const AsyncDeckLoader = require('../async-deck-loader')
const DefaultCardRenderer = require('../dumb-card-renderer')
const shuffle = require('knuth-shuffle').knuthShuffle
Expand All @@ -14,20 +14,20 @@ type ScoredCard = {
}

class LearningStrategy {
hpq: PouchQueue<ScoredCard>;
hpq: LevelQueue<ScoredCard>;
hpqCapacity: number;
lpq: PouchQueue<ScoredCard>;
done: PouchQueue<ScoredCard>;
backlog: PouchQueue<ScoredCard>;
lpq: LevelQueue<ScoredCard>;
done: LevelQueue<ScoredCard>;
backlog: LevelQueue<ScoredCard>;
learnedThreshold: number;
CardRenderer: any;

constructor (args: {
hpq: PouchQueue<ScoredCard>,
hpq: LevelQueue<ScoredCard>,
hpqCapacity?: number,
lpq: PouchQueue<ScoredCard>,
done: PouchQueue<ScoredCard>,
backlog: PouchQueue<ScoredCard>,
lpq: LevelQueue<ScoredCard>,
done: LevelQueue<ScoredCard>,
backlog: LevelQueue<ScoredCard>,
learnedThreshold?: number,
CardRenderer?: any
}) {
Expand All @@ -45,14 +45,14 @@ class LearningStrategy {
static async fromDeckName (
deckName: string,
options: {
Queue?: typeof PouchQueue,
Queue?: typeof LevelQueue,
deckLoader?: DeckLoader,
hpqCapacity?: number,
lpqCapacity?: number,
shuffleCards?: boolean
} = {}
): Promise<LearningStrategy> {
const Queue = options.Queue || PouchQueue
const Queue = options.Queue || LevelQueue
const deckLoader = options.deckLoader || new AsyncDeckLoader(deckName)
const hpqCapacity = options.hpqCapacity || DEFAULT_HPQ_CAPACITY
const lpqCapacity = options.lpqCapacity || DEFAULT_LPQ_CAPACITY
Expand Down Expand Up @@ -89,23 +89,24 @@ class LearningStrategy {
await deckLoader.setLoaded(true)
}

const db = deckLoader.getStorage()
const hpq = await Queue.initialize(hpqContents, {
dbName: deckName,
db,
queueName: 'hpq'
})

const lpq = await Queue.initialize(lpqContents, {
dbName: deckName,
db,
queueName: 'lpq'
})

const backlog = await Queue.initialize(backlogContents, {
dbName: deckName,
db,
queueName: 'backlog'
})

const done = await Queue.initialize(doneContents, {
dbName: deckName,
db,
queueName: 'done'
})

Expand Down
30 changes: 20 additions & 10 deletions lib/learning-strategy/learning-strategy.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -113,36 +113,41 @@ describe('learning-strategy', () => {
it('can instantiate a learning-strategy from an already loaded deck', async () => {
const deckName = 'spanish'

const mockStorage = mock({})
const mockDeckLoader = mock({
isLoaded: () => {},
getCardRenderer: () => {}
getCardRenderer: () => {},
getStorage: () => {}
})
.shouldReceive('getCardRenderer')
.once()
.shouldReceive('isLoaded')
.once()
.andReturn(true)
.shouldReceive('getStorage')
.once()
.andReturn(mockStorage)

// hackedy mock because mocha-mock doesn't support mocking classes right meow
const MockQueueClass = mock({initialize: () => {}})
.shouldReceive('initialize')
.with([], {
dbName: deckName,
db: mockStorage,
queueName: 'hpq'
})
.shouldReceive('initialize')
.with([], {
dbName: deckName,
db: mockStorage,
queueName: 'lpq'
})
.shouldReceive('initialize')
.with([], {
dbName: deckName,
db: mockStorage,
queueName: 'backlog'
})
.shouldReceive('initialize')
.with([], {
dbName: deckName,
db: mockStorage,
queueName: 'done'
})

Expand Down Expand Up @@ -185,11 +190,13 @@ describe('learning-strategy', () => {
correctCount: 0
}]

const mockStorage = mock({})
const mockDeckLoader = mock({
isLoaded: () => {},
load: () => {},
setLoaded: () => {},
getCardRenderer: () => {}
getCardRenderer: () => {},
getStorage: () => {}
})
.shouldReceive('getCardRenderer')
.once()
Expand All @@ -202,27 +209,30 @@ describe('learning-strategy', () => {
.shouldReceive('setLoaded')
.once()
.with(true)
.shouldReceive('getStorage')
.once()
.andReturn(mockStorage)

// hackedy mock because mocha-mock doesn't support mocking classes right meow :(
const MockQueueClass = mock({initialize: () => {}})
.shouldReceive('initialize')
.with(hpqContents, {
dbName: deckName,
db: mockStorage,
queueName: 'hpq'
})
.shouldReceive('initialize')
.with(lpqContents, {
dbName: deckName,
db: mockStorage,
queueName: 'lpq'
})
.shouldReceive('initialize')
.with(backlogContents, {
dbName: deckName,
db: mockStorage,
queueName: 'backlog'
})
.shouldReceive('initialize')
.with([], {
dbName: deckName,
db: mockStorage,
queueName: 'done'
})

Expand Down
2 changes: 2 additions & 0 deletions lib/level-db-wrapper/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/* @flow */
module.exports = require('./level-db-wrapper')
60 changes: 60 additions & 0 deletions lib/level-db-wrapper/level-db-wrapper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/* @flow */
const levelup = require('levelup')
const promisify = require('q-level')

type ConstructorOptions = {
adapter?: string
}

type DBConfig = {
valueEncoding: string,
db: Object
}

class LevelDBWrapper {
location: string;
adapter: ?string;
dbConfig: DBConfig;
db: Object;

constructor (location: string, options?: ConstructorOptions = {}) {
if (!location) {
throw new Error('Argument `location: string` is required!')
}

this.location = location
this.adapter = options.adapter

this.dbConfig = {
valueEncoding: 'json',
db: this.adapter === 'memory'
? require('memdown')
: require('leveldown')
}

this.db = promisify(levelup(this.location, this.dbConfig))
}

async get (key: string): Promise<any> {
return this.db.get(key)
}

async put (key: string, value: any): Promise<?Error> {
return this.db.put(key, value)
}

async destroy (): Promise<any> {
if (this.adapter === 'memory') {
return this.dbConfig.db.clearGlobalStore(true)
}

return new Promise((resolve, reject) => {
this.dbConfig.db.destroy(this.location, err => {
if (err) reject(err)
resolve()
})
})
}
}

module.exports = LevelDBWrapper
40 changes: 40 additions & 0 deletions lib/level-db-wrapper/level-db-wrapper.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/* @flow */
declare function describe(name:string, callback:Function):void;
declare function it(name:string, callback:Function):void;
declare function beforeEach(callback:Function):void;
declare function afterEach(callback:Function):void;

const DB = require('./')
const expect = require('expect')

describe('level-db-wrapper', function () {
beforeEach(function () {
const location = './mydb'
const options = {adapter: 'memory'}
this.db = new DB(location, options)
})

afterEach(function () {
return this.db.destroy()
})

it('throws an error when getting no existing thing', async function () {
let error
try {
await this.db.get('does not exist')
} catch (e) {
error = e
}

expect(error).toBeTruthy()
})

it('can put', async function () {
const key = 'key'
const value = 'value'
await this.db.put(key, value)
const got = await this.db.get(key)

expect(got).toBe(value)
})
})
2 changes: 2 additions & 0 deletions lib/level-queue/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/* @flow */
module.exports = require('./level-queue')
Loading

0 comments on commit f0d2d78

Please sign in to comment.