From db297c4a7833e1d5aab453541aa5b1751abd890b Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Tue, 17 Sep 2019 10:14:22 -0700 Subject: [PATCH] Use FNV instead of SHA512 for the hashing in A/B test (#7260) * Use FNV instead of SHA512 * Keep using `SHA512` algorithm for old experiments * Include LS to keep using the old algorithm * Use the expected list of old experiments * Added dispersion tests and changed npm package used * Code reviews * Code reviews --- news/2 Fixes/7218.md | 1 + package-lock.json | 10 +- package.json | 1 + src/client/common/crypto.ts | 11 +- src/client/common/experiments.ts | 9 +- src/client/common/types.ts | 2 +- src/test/common/crypto.unit.test.ts | 83 +- src/test/common/experiments.unit.test.ts | 29 +- src/test/common/randomWords.txt | 2000 ++++++++++++++++++++++ 9 files changed, 2135 insertions(+), 11 deletions(-) create mode 100644 news/2 Fixes/7218.md create mode 100644 src/test/common/randomWords.txt diff --git a/news/2 Fixes/7218.md b/news/2 Fixes/7218.md new file mode 100644 index 00000000000..499820e5d37 --- /dev/null +++ b/news/2 Fixes/7218.md @@ -0,0 +1 @@ +Fixed A/B testing sampling diff --git a/package-lock.json b/package-lock.json index bd199954fcf..38e74c1ec6d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1113,6 +1113,12 @@ "integrity": "sha512-rLu3wcBWH4P5q1CGoSSH/i9hrXs7SlbRLkoq9IGuoPYNGQvDJ3pt/wmOM+XgYjIDRMVIdkUWt0RsfzF50JfnCw==", "dev": true }, + "@enonic/fnv-plus": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@enonic/fnv-plus/-/fnv-plus-1.3.0.tgz", + "integrity": "sha512-BCN9uNWH8AmiP7BXBJqEinUY9KXalmRzo+L0cB/mQsmFfzODxwQrbvxCHXUNH2iP+qKkWYtB4vyy8N62PViMFw==", + "dev": true + }, "@gulp-sourcemaps/identity-map": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@gulp-sourcemaps/identity-map/-/identity-map-1.0.2.tgz", @@ -8114,7 +8120,7 @@ }, "event-stream": { "version": "3.3.4", - "resolved": "http://registry.npmjs.org/event-stream/-/event-stream-3.3.4.tgz", + "resolved": "https://registry.npmjs.org/event-stream/-/event-stream-3.3.4.tgz", "integrity": "sha1-SrTJoPWlTbkzi0w02Gv86PSzVXE=", "dev": true, "requires": { @@ -15547,7 +15553,7 @@ }, "queue": { "version": "3.1.0", - "resolved": "http://registry.npmjs.org/queue/-/queue-3.1.0.tgz", + "resolved": "https://registry.npmjs.org/queue/-/queue-3.1.0.tgz", "integrity": "sha1-bEnQHwCeIlZ4h4nyv/rGuLmZBYU=", "dev": true, "requires": { diff --git a/package.json b/package.json index fd6157991cf..9f2e0f4fea6 100644 --- a/package.json +++ b/package.json @@ -2662,6 +2662,7 @@ "@babel/preset-env": "^7.1.0", "@babel/preset-react": "^7.0.0", "@babel/register": "^7.4.4", + "@enonic/fnv-plus": "^1.3.0", "@istanbuljs/nyc-config-typescript": "^0.1.3", "@nteract/plotly": "^1.48.3", "@nteract/transform-dataresource": "^4.3.5", diff --git a/src/client/common/crypto.ts b/src/client/common/crypto.ts index c0115ddf8b9..a27adb13c9e 100644 --- a/src/client/common/crypto.ts +++ b/src/client/common/crypto.ts @@ -14,8 +14,15 @@ import { ICryptoUtils, IHashFormat } from './types'; */ @injectable() export class CryptoUtils implements ICryptoUtils { - public createHash(data: string, hashFormat: E): IHashFormat[E] { - const hash = createHash('sha512').update(data).digest('hex'); + public createHash(data: string, hashFormat: E, algorithm: 'SHA512' | 'FNV' = 'FNV'): IHashFormat[E] { + let hash: string; + if (algorithm === 'FNV') { + // tslint:disable-next-line:no-require-imports + const fnv = require('@enonic/fnv-plus'); + hash = fnv.fast1a32hex(data) as string; + } else { + hash = createHash('sha512').update(data).digest('hex'); + } if (hashFormat === 'number') { const result = parseInt(hash, 16); if (isNaN(result)) { diff --git a/src/client/common/experiments.ts b/src/client/common/experiments.ts index ef0f5db5241..4186429438c 100644 --- a/src/client/common/experiments.ts +++ b/src/client/common/experiments.ts @@ -31,6 +31,8 @@ export const downloadedExperimentStorageKey = 'DOWNLOADED_EXPERIMENTS_STORAGE_KE const configFile = path.join(EXTENSION_ROOT_DIR, 'experiments.json'); export const configUri = 'https://raw.githubusercontent.com/microsoft/vscode-python/master/experiments.json'; export const EXPERIMENTS_EFFORT_TIMEOUT_MS = 2000; +// The old experiments which are working fine using the `SHA512` algorithm +export const oldExperimentSalts = ['ShowExtensionSurveyPrompt', 'ShowPlayIcon', 'AlwaysDisplayTestExplorer', 'LS']; /** * Manages and stores experiments, implements the AB testing functionality @@ -160,7 +162,12 @@ export class ExperimentsManager implements IExperimentsManager { if (typeof (this.appEnvironment.machineId) !== 'string') { throw new Error('Machine ID should be a string'); } - const hash = this.crypto.createHash(`${this.appEnvironment.machineId}+${salt}`, 'number'); + let hash: number; + if (oldExperimentSalts.find(oldSalt => oldSalt === salt)) { + hash = this.crypto.createHash(`${this.appEnvironment.machineId}+${salt}`, 'number', 'SHA512'); + } else { + hash = this.crypto.createHash(`${this.appEnvironment.machineId}+${salt}`, 'number', 'FNV'); + } return hash % 100 >= min && hash % 100 < max; } diff --git a/src/client/common/types.ts b/src/client/common/types.ts index fe6513696b9..abc802bfc7d 100644 --- a/src/client/common/types.ts +++ b/src/client/common/types.ts @@ -507,7 +507,7 @@ export interface ICryptoUtils { * @param data The string to hash * @param hashFormat Return format of the hash, number or string */ - createHash(data: string, hashFormat: E): IHashFormat[E]; + createHash(data: string, hashFormat: E, algorithm?: 'SHA512' | 'FNV'): IHashFormat[E]; } export const IAsyncDisposableRegistry = Symbol('IAsyncDisposableRegistry'); diff --git a/src/test/common/crypto.unit.test.ts b/src/test/common/crypto.unit.test.ts index 6dfef4737ee..2d1ea5257cf 100644 --- a/src/test/common/crypto.unit.test.ts +++ b/src/test/common/crypto.unit.test.ts @@ -3,11 +3,18 @@ 'use strict'; -import { assert } from 'chai'; +import { assert, expect } from 'chai'; +import * as path from 'path'; import { CryptoUtils } from '../../client/common/crypto'; +import { FileSystem } from '../../client/common/platform/fileSystem'; +import { PlatformService } from '../../client/common/platform/platformService'; +import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../constants'; +// tslint:disable-next-line: max-func-body-length suite('Crypto Utils', async () => { let crypto: CryptoUtils; + const fs = new FileSystem(new PlatformService()); + const file = path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'test', 'common', 'randomWords.txt'); setup(() => { crypto = new CryptoUtils(); }); @@ -45,4 +52,78 @@ suite('Crypto Utils', async () => { const hash2 = crypto.createHash('hash2', 'string'); assert.notEqual(hash1, hash2, 'Hashes should be different strings'); }); + test('If hashFormat equals `number`, ensure numbers are uniformly distributed on scale from 0 to 100', async () => { + const words = await fs.readFile(file); + const wordList = words.split('\n'); + const buckets: number[] = Array(100).fill(0); + const hashes = Array(10).fill(0); + for (const w of wordList) { + for (let i = 0; i < 10; i += 1) { + const word = `${w}${i}`; + const hash = crypto.createHash(word, 'number'); + buckets[hash % 100] += 1; + hashes[i] = hash % 100; + } + } + // Total number of words = wordList.length * 10, because we added ten variants of each word above. + const expectedHitsPerBucket = wordList.length * 10 / 100; + for (const hit of buckets) { + expect(hit).to.be.lessThan(1.25 * expectedHitsPerBucket); + expect(hit).to.be.greaterThan(0.75 * expectedHitsPerBucket); + } + }); + test('If hashFormat equals `number`, on a scale of 0 to 100, small difference in the input on average produce large differences (about 33) in the output ', async () => { + const words = await fs.readFile(file); + const wordList = words.split('\n'); + const buckets: number[] = Array(100).fill(0); + let hashes: number[] = []; + let totalDifference = 0; + // We are only iterating over the first 10 words for purposes of this test + for (const w of wordList.slice(0, 10)) { + hashes = []; + totalDifference = 0; + if (w.length === 0) { + continue; + } + for (let i = 0; i < 10; i += 1) { + const word = `${w}${i}`; + const hash = crypto.createHash(word, 'number'); + buckets[hash % 100] += 1; + hashes.push(hash % 100); + } + for (let i = 0; i < 10; i += 1) { + const word = `${i}${w}`; + const hash = crypto.createHash(word, 'number'); + buckets[hash % 100] += 1; + hashes.push(hash % 100); + } + // Iterating over ASCII alphabets 'a' to 'z' and appending to the word + for (let i = 0; i < 26; i += 1) { + const word = `${String.fromCharCode(97 + i)}${w}`; + const hash = crypto.createHash(word, 'number'); + buckets[hash % 100] += 1; + hashes.push(hash % 100); + } + // Iterating over ASCII alphabets 'a' to 'z' and prepending to the word + for (let i = 0; i < 26; i += 1) { + const word = `${w}${String.fromCharCode(97 + i)}`; + const hash = crypto.createHash(word, 'number'); + buckets[hash % 100] += 1; + hashes.push(hash % 100); + } + // tslint:disable: prefer-for-of + for (let i = 0; i < hashes.length; i += 1) { + for (let j = 0; j < hashes.length; j += 1) { + if (hashes[i] > hashes[j]) { + totalDifference += hashes[i] - hashes[j]; + } else { + totalDifference += hashes[j] - hashes[i]; + } + } + } + const averageDifference = totalDifference / hashes.length / hashes.length; + expect(averageDifference).to.be.lessThan(1.25 * 33); + expect(averageDifference).to.be.greaterThan(0.75 * 33); + } + }); }); diff --git a/src/test/common/experiments.unit.test.ts b/src/test/common/experiments.unit.test.ts index 540393489c1..4854f5929c1 100644 --- a/src/test/common/experiments.unit.test.ts +++ b/src/test/common/experiments.unit.test.ts @@ -14,7 +14,7 @@ import { ApplicationEnvironment } from '../../client/common/application/applicat import { IApplicationEnvironment, IWorkspaceService } from '../../client/common/application/types'; import { WorkspaceService } from '../../client/common/application/workspace'; import { CryptoUtils } from '../../client/common/crypto'; -import { configUri, downloadedExperimentStorageKey, ExperimentsManager, experimentStorageKey, isDownloadedStorageValidKey } from '../../client/common/experiments'; +import { configUri, downloadedExperimentStorageKey, ExperimentsManager, experimentStorageKey, isDownloadedStorageValidKey, oldExperimentSalts } from '../../client/common/experiments'; import { HttpClient } from '../../client/common/net/httpClient'; import { PersistentStateFactory } from '../../client/common/persistentState'; import { FileSystem } from '../../client/common/platform/fileSystem'; @@ -596,14 +596,35 @@ suite('xA/B experiments', () => { expect(() => expManager.isUserInRange(79, 94, 'salt')).to.throw(); } else if (testParams.error) { const error = new Error('Kaboom'); - when(crypto.createHash(anything(), 'number')).thenThrow(error); + when(crypto.createHash(anything(), 'number', anything())).thenThrow(error); expect(() => expManager.isUserInRange(79, 94, 'salt')).to.throw(error); } else { - when(crypto.createHash(anything(), 'number')).thenReturn(testParams.hash); + when(crypto.createHash(anything(), 'number', anything())).thenReturn(testParams.hash); expect(expManager.isUserInRange(79, 94, 'salt')).to.equal(testParams.expectedResult, 'Incorrectly identified'); } }); }); + test('If experiment salt belongs to an old experiment, keep using `SHA512` algorithm', async () => { + when(appEnvironment.machineId).thenReturn('101'); + when(crypto.createHash(anything(), 'number', 'SHA512')).thenReturn(644); + when(crypto.createHash(anything(), anything(), 'FNV')).thenReturn(1293); + // 'ShowPlayIcon' is one of the old experiments + expManager.isUserInRange(79, 94, 'ShowPlayIcon'); + verify(crypto.createHash(anything(), 'number', 'SHA512')).once(); + verify(crypto.createHash(anything(), anything(), 'FNV')).never(); + }); + test('If experiment salt does not belong to an old experiment, use `FNV` algorithm', async () => { + when(appEnvironment.machineId).thenReturn('101'); + when(crypto.createHash(anything(), anything(), 'SHA512')).thenReturn(644); + when(crypto.createHash(anything(), 'number', 'FNV')).thenReturn(1293); + expManager.isUserInRange(79, 94, 'NewExperimentSalt'); + verify(crypto.createHash(anything(), anything(), 'SHA512')).never(); + verify(crypto.createHash(anything(), 'number', 'FNV')).once(); + }); + test('Use the expected list of old experiments', async () => { + const expectedOldExperimentSalts = ['ShowExtensionSurveyPrompt', 'ShowPlayIcon', 'AlwaysDisplayTestExplorer', 'LS']; + assert.deepEqual(expectedOldExperimentSalts, oldExperimentSalts); + }); }); const testsForPopulateUserExperiments = @@ -633,7 +654,7 @@ suite('xA/B experiments', () => { .returns(() => testParams.experimentStorageValue); when(appEnvironment.machineId).thenReturn('101'); if (testParams.hash) { - when(crypto.createHash(anything(), 'number')).thenReturn(testParams.hash); + when(crypto.createHash(anything(), 'number', anything())).thenReturn(testParams.hash); } expManager.populateUserExperiments(); assert.deepEqual(expManager.userExperiments, testParams.expectedResult); diff --git a/src/test/common/randomWords.txt b/src/test/common/randomWords.txt new file mode 100644 index 00000000000..56066eaa957 --- /dev/null +++ b/src/test/common/randomWords.txt @@ -0,0 +1,2000 @@ +screw +passenger +zesty +concerned +rustic +store +disagreeable +own +tranquil +modern +tickle +ceaseless +responsible +exclusive +harass +book +attach +squeak +amount +describe +deer +burst +women +influence +undesirable +jewel +inject +balance +dysfunctional +dog +recess +caption +abusive +hallowed +fabulous +maniacal +sweltering +adventurous +glorious +shut +carpenter +sun +kneel +impartial +ashamed +joke +therapeutic +friendly +wood +comfortable +repeat +pencil +agonizing +pricey +territory +scream +shrill +fry +invite +color +strange +zippy +plate +exist +succinct +wholesale +macabre +jam +cloudy +design +stone +apologise +snotty +ruddy +penitent +ban +eager +marry +neat +stale +angry +historical +park +club +cumbersome +table +kitty +parsimonious +sidewalk +dress +truck +ants +odd +worry +roll +stupid +jeans +desert +drop +nod +disastrous +gate +dreary +twist +plane +sky +piquant +naughty +complete +house +add +fool +hate +owe +stuff +humorous +kill +strip +dust +bump +moldy +separate +chalk +fly +third +guarded +sand +three +structure +tease +dispensable +beneficial +comb +attack +undress +bath +scarecrow +gusty +incredible +quaint +dream +wait +rainy +accept +tan +brass +sad +delay +ducks +joyous +trucks +tidy +redundant +unpack +square +north +belligerent +enthusiastic +utopian +last +zinc +shoe +reminiscent +offbeat +army +help +ear +draconian +religion +spark +yarn +spotty +moaning +polish +bite-sized +crayon +mess up +smile +endurable +nut +pedal +root +synonymous +complete +rotten +obedient +flippant +potato +twist +gratis +fresh +vague +slim +empty +grain +uttermost +warm +violet +harm +dad +crack +strap +animated +detect +aback +death +jail +announce +spooky +watch +wonder +unbecoming +zealous +gentle +quiver +royal +shade +attractive +crazy +live +courageous +zoo +solid +rice +applaud +willing +leather +friend +permit +plant +destroy +typical +tight +change +rabbit +behavior +oil +eyes +malicious +axiomatic +exercise +lunchroom +rod +spot +different +delightful +tire +ragged +juicy +tacky +corn +painstaking +tangible +gigantic +ground +curved +ablaze +messy +thick +truculent +paste +mellow +bashful +recognise +join +pull +obsolete +name +price +mixed +overwrought +plan +lick +five +creature +protect +daily +frequent +cynical +icicle +lock +insidious +rough +grubby +credit +challenge +descriptive +wet +introduce +notice +boil +zip +stop +gamy +star +wine +slap +measure +impossible +realise +concentrate +swim +drink +texture +calm +run +rhetorical +whine +page +mark +confused +ill-informed +diligent +good +ball +pause +befitting +toothbrush +bee +fancy +flower +elegant +rule +deafening +heartbreaking +purple +temper +scrape +plant +number +drain +arm +youthful +shame +snow +chop +event +advertisement +wiry +bikes +bat +mate +coach +nifty +parallel +degree +romantic +wanting +battle +meaty +full +unit +blade +wrestle +hook +wakeful +foolish +place +gaze +precede +volatile +replace +chivalrous +adjustment +idea +agree +eye +skinny +reward +grandfather +apparatus +pat +private +square +chief +brick +bomb +bulb +melt +form +snails +tent +giant +treatment +pail +shape +spiky +thoughtful +mean +disillusioned +sophisticated +lively +murky +tank +needle +harbor +gaping +subdued +momentous +dirty +married +secretive +frightened +easy +consider +scarce +absorbed +hammer +icky +metal +stocking +pathetic +son +car +crook +stiff +look +familiar +quirky +numerous +calendar +green +aunt +aromatic +air +complex +reply +health +current +observation +nation +burly +cannon +regret +listen +rings +door +level +sniff +unsightly +alert +doctor +false +desire +support +hammer +maddening +tasteless +secretary +special +earthy +argue +connection +hurry +smell +tour +cows +room +string +placid +confess +true +hypnotic +meal +caring +sore +swift +cup +fretful +peep +stay +scandalous +disarm +leg +material +arrange +strong +man +scorch +swing +society +quiet +peace +dynamic +flowers +helpful +breath +young +kindhearted +wind +glossy +knot +cooing +vegetable +idiotic +aboard +nutty +near +claim +bite +trick +preserve +mountainous +imagine +uninterested +enter +rat +gainful +prickly +coach +alluring +money +mouth +sip +fear +plantation +conscious +unequaled +jaded +appreciate +shivering +bake +weigh +labored +feigned +straight +end +act +consist +victorious +mountain +sleep +dinosaurs +trip +grate +last +regular +tiresome +whistle +gather +maid +pretend +weather +abandoned +drag +enjoy +foregoing +glove +boundary +weight +smelly +tug +butter +fit +manage +unarmed +steady +sassy +depressed +secret +abject +loud +fear +government +blood +alive +soak +wicked +bright +note +touch +innate +walk +jump +use +supreme +suspect +haunt +lethal +needy +seashore +colour +available +curious +brush +loose +cellar +push +evanescent +trace +cultured +ubiquitous +plot +wild +seemly +enchanting +milky +cure +lake +hospital +evasive +puzzling +woozy +lowly +acoustics +wax +madly +distance +bare +jump +van +decision +cheese +suggest +salt +houses +bury +spray +value +woman +raise +nest +fortunate +pass +efficient +stretch +interest +tray +pop +bounce +aspiring +busy +enormous +porter +yawn +frogs +immense +water +late +size +man +instinctive +whistle +skin +pot +zany +surprise +bubble +seal +nervous +lunch +combative +dazzling +feeble +enchanted +analyse +unique +provide +great +high-pitched +scare +arrive +push +signal +business +approve +steel +eight +common +windy +marked +sloppy +warm +fair +remain +sigh +knowing +frog +oceanic +tub +spectacular +knot +prepare +cover +tremendous +silent +stitch +shock +moon +calculate +representative +gleaming +dramatic +top +freezing +inquisitive +round +knife +oval +pack +fairies +taboo +ad hoc +abundant +unadvised +verse +condemned +tall +tap +confuse +sleet +peck +long +holiday +veil +lucky +produce +cautious +yoke +dear +industrious +present +political +mix +rejoice +lively +river +serve +cars +lush +zebra +loutish +sink +flavor +finger +flowery +yellow +marble +jealous +clip +dashing +pleasant +likeable +difficult +scene +quilt +forgetful +devilish +point +acrid +awake +imaginary +trouble +excuse +mourn +hat +town +puzzled +null +warlike +real +toothpaste +sleepy +lopsided +clumsy +uneven +lonely +harmonious +hospitable +temporary +avoid +trade +rock +deadpan +stranger +request +acidic +bone +actor +chilly +wheel +tie +rub +wall +chew +grab +clear +splendid +ghost +attraction +board +tip +aquatic +shop +orange +agreeable +branch +glass +line +increase +hulking +order +decorous +basketball +spot +monkey +cloistered +dust +ocean +scientific +camp +minor +skillful +worm +mom +divergent +pick +meeting +turn +expert +holistic +grey +sort +blushing +tart +touch +rainstorm +crush +field +upbeat +tangy +wish +handy +spotless +steep +afford +salty +snobbish +groan +uptight +question +drain +glistening +discover +wash +unkempt +funny +waste +fix +poison +allow +decisive +arrogant +robust +elastic +turkey +red +calculating +edge +old-fashioned +inexpensive +ticket +route +wail +dry +basin +squeamish +frighten +cart +elderly +murder +lavish +best +brown +glow +slave +tail +towering +scale +flame +alert +please +slope +direful +cactus +second-hand +macho +prevent +release +quarter +excite +party +trousers +test +defeated +long +throne +irritating +zoom +chase +afternoon +suffer +train +balance +station +force +smile +elfin +breakable +cheat +toes +tame +cute +medical +shallow +well-to-do +flimsy +smoke +blue-eyed +cloth +notebook +lettuce +ray +low +productive +wobble +existence +cow +wink +energetic +disappear +boy +lamp +distinct +illegal +addicted +aboriginal +yam +clean +black +hate +plug +comparison +rush +next +volleyball +distribution +sneaky +concern +precious +self +building +wound +psychotic +flap +same +swanky +quixotic +jail +oven +jumbled +past +spill +home +drown +impulse +imminent +sweet +helpless +didactic +turn +savory +ski +opposite +plant +stop +mint +wandering +appliance +repair +fluffy +eminent +lewd +physical +pig +regret +stomach +extra-small +stain +ugliest +cake +separate +partner +water +ring +end +envious +futuristic +itch +rifle +unite +puny +horn +reading +embarrassed +halting +channel +watery +wonderful +gruesome +point +shocking +relax +subtract +economic +luxuriant +parcel +radiate +wary +rich +stare +wasteful +six +nasty +quick +creepy +highfalutin +spotted +cobweb +explode +subsequent +blink +activity +plough +report +rabid +eggs +bouncy +quarrelsome +produce +street +spy +frantic +steadfast +strengthen +head +sour +unused +matter +jazzy +slow +tearful +nebulous +accidental +wool +impolite +simplistic +quicksand +spoil +meat +intelligent +scary +scarf +permissible +command +kettle +grade +animal +purring +crash +annoying +vein +duck +elbow +step +slippery +juvenile +war +ignorant +fuel +pigs +smash +peaceful +astonishing +lock +questionable +obtainable +stupendous +income +hour +love +sick +rate +compete +bent +servant +melted +blind +thing +obeisant +flesh +coherent +wooden +arch +cause +flow +understood +earn +gifted +wave +straw +skirt +boring +extra-large +daffy +detailed +tired +dogs +blue +possible +fish +makeshift +attack +bang +peel +magic +debonair +receive +orange +wise +ill-fated +striped +nail +belief +furniture +group +motionless +sugar +surround +tested +coal +question +well-off +squeal +waggish +wrist +actually +trains +bruise +show +ill +equal +earth +volcano +rambunctious +unusual +pocket +language +thought +parched +camera +pastoral +aggressive +land +learn +hurried +quizzical +bit +ignore +front +fade +discussion +mice +ambitious +abaft +suit +various +ink +dance +reduce +screeching +apparel +delicate +faithful +decide +low +staking +probable +curve +delicious +trade +drab +steer +argument +collect +sheep +anxious +search +brake +card +squealing +sprout +amazing +tree +farm +narrow +tense +books +gullible +alike +pumped +melodic +satisfying +shop +improve +button +irate +big +thin +drop +curl +umbrella +talk +marvelous +level +stir +tenuous +yummy +arithmetic +overt +pull +gray +large +rule +teeny-tiny +shelter +scared +judge +wacky +cakes +merciful +testy +shave +limping +power +abiding +bless +dapper +internal +division +taste +donkey +airplane +dolls +ethereal +spiteful +smoke +bear +knowledgeable +like +delight +picayune +toe +apathetic +wealthy +sponge +sail +crow +slip +loss +weak +pointless +queen +ship +letters +pollution +upset +aberrant +yard +bumpy +pin +pushy +waste +expand +vacuous +fierce +determined +discreet +lip +paint +stingy +vest +amusing +two +nappy +hungry +wilderness +offer +kindly +connect +employ +neighborly +dare +open +planes +cat +office +voyage +float +festive +cracker +adaptable +ludicrous +omniscient +guiltless +heavenly +even +name +appear +crowded +homely +kaput +stick +spiffy +classy +disgusting +heat +thirsty +nimble +invincible +shiny +paper +songs +able +tasteful +open +mighty +chemical +trot +flag +sincere +wren +known +tempt +afraid +squirrel +exultant +ordinary +quill +sound +thunder +haircut +lame +beef +airport +cut +vigorous +boat +prefer +disagree +race +bubble +sore +famous +baby +accessible +tumble +callous +whirl +rob +lackadaisical +view +bike +seed +mother +jar +used +risk +move +yell +groovy +vast +protest +normal +wide-eyed +paddle +bell +charming +nerve +delirious +overconfident +teeny +choke +pleasure +elite +capricious +sin +snore +mine +lie +call +resolute +bathe +dry +dock +careful +program +birds +mere +neck +second +scratch +spiritual +little +x-ray +greasy +cattle +ripe +property +snakes +crooked +aware +cooperative +plastic +observant +expansion +sedate +class +geese +first +industry +knee +change +hard-to-find +intend +icy +scent +obsequious +hum +form +happy +relation +detail +person +science +reign +addition +shade +possess +mysterious +sister +teeth +remember +telling +outstanding +repulsive +soothe +succeed +scrub +rebel +morning +crawl +hobbies +alleged +middle +old +absurd +nose +polite +anger +erratic +part +memory +alcoholic +picture +vanish +small +fire +mass +obscene +tendency +daughter +decay +drunk +rain +muddle +sudden +hover +pen +poor +embarrass +judge +carriage +cool +land +cheap +error +damage +periodic +thumb +guitar +engine +waiting +fertile +unaccountable +correct +fetch +skip +base +educate +nonchalant +racial +double +continue +painful +type +cave +steam +roasted +clean +cycle +borrow +rapid +automatic +bait +tin +saw +development +walk +suggestion +judicious +time +bird +clap +deeply +inconclusive +vulgar +cast +sneeze +bleach +nosy +explain +settle +military +trashy +ruthless +cemetery +book +cluttered +pets +unable +mark +thoughtless +fork +thankful +foamy +seat +smell +writing +eggnog +care +shaky +breezy +unruly +lying +chunky +hope +brother +shirt +panoramic +truthful +education +condition +psychedelic +extend +deliver +miniature +rain +oatmeal +voiceless +hot +mammoth +finger +empty +smart +guide +direction +gorgeous +position +friends +trap +zonked +oranges +adhesive +order +boundless +public +telephone +fascinated +noxious +rhythm +zephyr +tongue +organic +tense +knowledge +fold +vengeful +authority +faulty +head +dusty +bow +ambiguous +sneeze +broken +sharp +spell +poised +egg +fragile +stamp +company +load +ancient +somber +believe +fearless +thread +kick +compare +beam +interest +sordid +hard +infamous +impress +earthquake +action +ready +superficial +contain +spring +colorful +humdrum +certain +tricky +bitter +scatter +laugh +greedy +silly +join +prick +four +crate +jittery +bead +giraffe +whip +kick +needless +rinse +rot +history +roll +boot +hellish +instrument +object +lovely +tame +trite +majestic +rescue +superb +ten +frail +stage +spicy +crib +brake +pies +sign +flood +gun +trust +preach +ugly +abrupt +unhealthy +wave +drawer +grass +bloody +shock +hanging +versed +window +workable +suit +sulky +mindless +few +disgusted +achiever +art +verdant +lacking +flagrant +materialistic +grandmother +frame +save +thrill +tiny +reflect +nonstop +jog +wrathful +advise +righteous +massive +numberless +magnificent +cheerful +left +protective +talk +lace +nauseating +fearful +month +obnoxious +selfish +soda +plain +meddle +can +absorbing +rock +hollow +weary +cable +beautiful +awesome +glib +harmony +frightening +ladybug +occur +abhorrent +dress +powder +example +carry +experience +dizzy +noise +mushy +baseball +cross +jelly +heavy +hose +entertaining +store +moan +ahead +changeable +unknown +drum +hand +pale +mature +work +grip +control +grape +jam +sweater +nippy +muddled +lazy +whole +useless +start +fast +advice +simple +want +tremble +many +learned +terrific +bag +symptomatic +pray +tiger +outrageous +theory +resonant +sack +hushed +hysterical +match +care +support +cabbage +beginner +committee +voracious +spurious +miss +silky +profit +whisper +noisy +thundering +horse +tacit +sail +scissors +thaw +domineering +trouble +box +discovery +childlike +cuddly +perpetual +husky +fruit +scold +elated +godly +guarantee +nutritious +hesitant +doubt +cherries +curly +cough +move +bottle +clear +ratty +stretch +stormy +overflow +puffy +tick +harsh +female +test +illustrious +expensive +muscle +attend +stereotyped +payment +deep +afterthought +pear +quiet +launch +suppose +examine +worried +selective +flower +motion +divide +wriggle +warn +flashy +hateful +milk +hideous +post +unbiased +rural +remind +transport +fancy +list +day +reaction +thinkable +absent +grieving +increase +cream +thank +interrupt +bewildered +aftermath +misty +mind +grease +cover +overjoyed +develop +deceive +growth +treat +complain +pine +wish +twig +box +heady +hall +previous +liquid +aloof +dull +trees +present +wipe +key +jobless +careless +week +mute +curvy +imported +need +puncture +whip +title +finicky +pancake +unwritten +suck +acceptable +valuable +play +quack +wretched +magenta +shoes +wry +vacation +deserve +coil +grotesque +wide +fixed +womanly +rare +wire +heap +badge +honorable +irritate +bawdy +supply +sheet +erect +frame +hilarious +colossal +bed +girl +pet +crabby +cry +deranged +wistful +plucky +pump +cold +shake +satisfy +safe +handsomely +faded +follow +serious +dangerous +insect +annoy +loaf +soap +taste +mitten +lyrical +substantial +fog +wrench +destruction +lighten +wrap +soggy +hot +terrible +bedroom +fanatical +receipt