Skip to content

Commit d8b6b31

Browse files
committed
Add support for cloud storage compression
Cloud storage is a limited resource, and thus it makes sense to support data compression before sending the data to cloud storage. A new hidden setting allows to toggle on cloud storage compression: name: cloudStorageCompression default: false By default, this hidden setting is `false`, and a user must set it to `true` to enable compression of cloud storage items. This hidden setting will eventually be toggled to `true` by default, when there is good confidence a majority of users are using a version of uBO which can properly handle compressed cloud storage items. A cursory assessment shows that compressed items are roughly 40-50% smaller in size.
1 parent de6a9e3 commit d8b6b31

File tree

7 files changed

+367
-206
lines changed

7 files changed

+367
-206
lines changed

platform/chromium/vapi-background.js

Lines changed: 37 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1540,10 +1540,10 @@ vAPI.cloud = (( ) => {
15401540
// good thing given chrome.storage.sync.MAX_WRITE_OPERATIONS_PER_MINUTE
15411541
// and chrome.storage.sync.MAX_WRITE_OPERATIONS_PER_HOUR.
15421542

1543-
const getCoarseChunkCount = async function(dataKey) {
1543+
const getCoarseChunkCount = async function(datakey) {
15441544
const keys = {};
15451545
for ( let i = 0; i < maxChunkCountPerItem; i += 16 ) {
1546-
keys[dataKey + i.toString()] = '';
1546+
keys[datakey + i.toString()] = '';
15471547
}
15481548
let bin;
15491549
try {
@@ -1553,13 +1553,13 @@ vAPI.cloud = (( ) => {
15531553
}
15541554
let chunkCount = 0;
15551555
for ( let i = 0; i < maxChunkCountPerItem; i += 16 ) {
1556-
if ( bin[dataKey + i.toString()] === '' ) { break; }
1556+
if ( bin[datakey + i.toString()] === '' ) { break; }
15571557
chunkCount = i + 16;
15581558
}
15591559
return chunkCount;
15601560
};
15611561

1562-
const deleteChunks = function(dataKey, start) {
1562+
const deleteChunks = function(datakey, start) {
15631563
const keys = [];
15641564

15651565
// No point in deleting more than:
@@ -1570,34 +1570,37 @@ vAPI.cloud = (( ) => {
15701570
Math.ceil(maxStorageSize / maxChunkSize)
15711571
);
15721572
for ( let i = start; i < n; i++ ) {
1573-
keys.push(dataKey + i.toString());
1573+
keys.push(datakey + i.toString());
15741574
}
15751575
if ( keys.length !== 0 ) {
15761576
webext.storage.sync.remove(keys);
15771577
}
15781578
};
15791579

1580-
const push = async function(dataKey, data) {
1581-
let bin = {
1582-
'source': options.deviceName || options.defaultDeviceName,
1583-
'tstamp': Date.now(),
1584-
'data': data,
1585-
'size': 0
1580+
const push = async function(details) {
1581+
const { datakey, data, encode } = details;
1582+
const item = {
1583+
source: options.deviceName || options.defaultDeviceName,
1584+
tstamp: Date.now(),
1585+
data,
15861586
};
1587-
bin.size = JSON.stringify(bin).length;
1588-
const item = JSON.stringify(bin);
1587+
const json = JSON.stringify(item);
1588+
const encoded = encode instanceof Function
1589+
? await encode(json)
1590+
: json;
15891591

15901592
// Chunkify taking into account QUOTA_BYTES_PER_ITEM:
15911593
// https://developer.chrome.com/extensions/storage#property-sync
15921594
// "The maximum size (in bytes) of each individual item in sync
15931595
// "storage, as measured by the JSON stringification of its value
15941596
// "plus its key length."
1595-
bin = {};
1596-
let chunkCount = Math.ceil(item.length / maxChunkSize);
1597+
const bin = {};
1598+
const chunkCount = Math.ceil(encoded.length / maxChunkSize);
15971599
for ( let i = 0; i < chunkCount; i++ ) {
1598-
bin[dataKey + i.toString()] = item.substr(i * maxChunkSize, maxChunkSize);
1600+
bin[datakey + i.toString()]
1601+
= encoded.substr(i * maxChunkSize, maxChunkSize);
15991602
}
1600-
bin[dataKey + chunkCount.toString()] = ''; // Sentinel
1603+
bin[datakey + chunkCount.toString()] = ''; // Sentinel
16011604

16021605
try {
16031606
await webext.storage.sync.set(bin);
@@ -1606,18 +1609,19 @@ vAPI.cloud = (( ) => {
16061609
}
16071610

16081611
// Remove potentially unused trailing chunks
1609-
deleteChunks(dataKey, chunkCount);
1612+
deleteChunks(datakey, chunkCount);
16101613
};
16111614

1612-
const pull = async function(dataKey) {
1615+
const pull = async function(details) {
1616+
const { datakey, decode } = details;
16131617

1614-
const result = await getCoarseChunkCount(dataKey);
1618+
const result = await getCoarseChunkCount(datakey);
16151619
if ( typeof result !== 'number' ) {
16161620
return result;
16171621
}
16181622
const chunkKeys = {};
16191623
for ( let i = 0; i < result; i++ ) {
1620-
chunkKeys[dataKey + i.toString()] = '';
1624+
chunkKeys[datakey + i.toString()] = '';
16211625
}
16221626

16231627
let bin;
@@ -1633,31 +1637,35 @@ vAPI.cloud = (( ) => {
16331637
// happen when the number of chunks is a multiple of
16341638
// chunkCountPerFetch. Hence why we must also test against
16351639
// undefined.
1636-
let json = [], jsonSlice;
1640+
let encoded = [];
16371641
let i = 0;
16381642
for (;;) {
1639-
jsonSlice = bin[dataKey + i.toString()];
1640-
if ( jsonSlice === '' || jsonSlice === undefined ) { break; }
1641-
json.push(jsonSlice);
1643+
const slice = bin[datakey + i.toString()];
1644+
if ( slice === '' || slice === undefined ) { break; }
1645+
encoded.push(slice);
16421646
i += 1;
16431647
}
1648+
encoded = encoded.join('');
1649+
const json = decode instanceof Function
1650+
? await decode(encoded)
1651+
: encoded;
16441652
let entry = null;
16451653
try {
1646-
entry = JSON.parse(json.join(''));
1654+
entry = JSON.parse(json);
16471655
} catch(ex) {
16481656
}
16491657
return entry;
16501658
};
16511659

1652-
const used = async function(dataKey) {
1660+
const used = async function(datakey) {
16531661
if ( webext.storage.sync.getBytesInUse instanceof Function === false ) {
16541662
return;
16551663
}
1656-
const coarseCount = await getCoarseChunkCount(dataKey);
1664+
const coarseCount = await getCoarseChunkCount(datakey);
16571665
if ( typeof coarseCount !== 'number' ) { return; }
16581666
const keys = [];
16591667
for ( let i = 0; i < coarseCount; i++ ) {
1660-
keys.push(`${dataKey}${i}`);
1668+
keys.push(`${datakey}${i}`);
16611669
}
16621670
let results;
16631671
try {

src/js/background.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ const µBlock = (( ) => { // jshint ignore:line
4747
cacheStorageAPI: 'unset',
4848
cacheStorageCompression: true,
4949
cacheControlForFirefox1376932: 'no-cache, no-store, must-revalidate',
50+
cloudStorageCompression: false,
5051
cnameIgnoreList: 'unset',
5152
cnameIgnore1stParty: true,
5253
cnameIgnoreExceptions: true,

src/js/cachestorage.js

Lines changed: 43 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -195,22 +195,48 @@
195195
return dbPromise;
196196
};
197197

198+
const fromBlob = function(data) {
199+
if ( data instanceof Blob === false ) {
200+
return Promise.resolve(data);
201+
}
202+
return new Promise(resolve => {
203+
const blobReader = new FileReader();
204+
blobReader.onloadend = ev => {
205+
resolve(new Uint8Array(ev.target.result));
206+
};
207+
blobReader.readAsArrayBuffer(data);
208+
});
209+
};
210+
211+
const toBlob = function(data) {
212+
const value = data instanceof Uint8Array
213+
? new Blob([ data ])
214+
: data;
215+
return Promise.resolve(value);
216+
};
217+
218+
const compress = function(store, key, data) {
219+
return µBlock.lz4Codec.encode(data, toBlob).then(value => {
220+
store.push({ key, value });
221+
});
222+
};
223+
224+
const decompress = function(store, key, data) {
225+
return µBlock.lz4Codec.decode(data, fromBlob).then(data => {
226+
store[key] = data;
227+
});
228+
};
229+
198230
const getFromDb = async function(keys, keyvalStore, callback) {
199231
if ( typeof callback !== 'function' ) { return; }
200232
if ( keys.length === 0 ) { return callback(keyvalStore); }
201233
const promises = [];
202234
const gotOne = function() {
203235
if ( typeof this.result !== 'object' ) { return; }
204-
keyvalStore[this.result.key] = this.result.value;
205-
if ( this.result.value instanceof Blob === false ) { return; }
206-
promises.push(
207-
µBlock.lz4Codec.decode(
208-
this.result.key,
209-
this.result.value
210-
).then(result => {
211-
keyvalStore[result.key] = result.data;
212-
})
213-
);
236+
const { key, value } = this.result;
237+
keyvalStore[key] = value;
238+
if ( value instanceof Blob === false ) { return; }
239+
promises.push(decompress(keyvalStore, key, value));
214240
};
215241
try {
216242
const db = await getDb();
@@ -265,16 +291,10 @@
265291
});
266292
return;
267293
}
268-
keyvalStore[entry.key] = entry.value;
294+
const { key, value } = entry;
295+
keyvalStore[key] = value;
269296
if ( entry.value instanceof Blob === false ) { return; }
270-
promises.push(
271-
µBlock.lz4Codec.decode(
272-
entry.key,
273-
entry.value
274-
).then(result => {
275-
keyvalStore[result.key] = result.value;
276-
})
277-
);
297+
promises.push(decompress(keyvalStore, key, value));
278298
}).catch(reason => {
279299
console.info(`cacheStorage.getAllFromDb() failed: ${reason}`);
280300
callback();
@@ -297,19 +317,14 @@
297317
const entries = [];
298318
const dontCompress =
299319
µBlock.hiddenSettings.cacheStorageCompression !== true;
300-
const handleEncodingResult = result => {
301-
entries.push({ key: result.key, value: result.data });
302-
};
303320
for ( const key of keys ) {
304-
const data = keyvalStore[key];
305-
const isString = typeof data === 'string';
321+
const value = keyvalStore[key];
322+
const isString = typeof value === 'string';
306323
if ( isString === false || dontCompress ) {
307-
entries.push({ key, value: data });
324+
entries.push({ key, value });
308325
continue;
309326
}
310-
promises.push(
311-
µBlock.lz4Codec.encode(key, data).then(handleEncodingResult)
312-
);
327+
promises.push(compress(entries, key, value));
313328
}
314329
const finish = ( ) => {
315330
if ( callback === undefined ) { return; }

src/js/cloud-ui.js

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ if ( self.cloud.datakey === '' ) { return; }
4848
/******************************************************************************/
4949

5050
const fetchStorageUsed = async function() {
51-
const elem = widget.querySelector('#cloudCapacity');
51+
let elem = widget.querySelector('#cloudCapacity');
5252
if ( elem.classList.contains('hide') ) { return; }
5353
const result = await vAPI.messaging.send('cloudWidget', {
5454
what: 'cloudUsed',
@@ -58,10 +58,16 @@ const fetchStorageUsed = async function() {
5858
elem.classList.add('hide');
5959
return;
6060
}
61+
const units = ' ' + vAPI.i18n('genericBytes');
62+
elem.title = result.max.toLocaleString() + units;
6163
const total = (result.total / result.max * 100).toFixed(1);
62-
elem.firstElementChild.style.width = `${total}%`;
64+
elem = elem.firstElementChild;
65+
elem.style.width = `${total}%`;
66+
elem.title = result.total.toLocaleString() + units;
6367
const used = (result.used / result.total * 100).toFixed(1);
64-
elem.firstElementChild.firstElementChild.style.width = `${used}%`;
68+
elem = elem.firstElementChild;
69+
elem.style.width = `${used}%`;
70+
elem.title = result.used.toLocaleString() + units;
6571
};
6672

6773
/******************************************************************************/

0 commit comments

Comments
 (0)