Skip to content

Commit

Permalink
Improve memory efficiency when downloading matrix data. See issue #502.
Browse files Browse the repository at this point in the history
The browser can fail with an out-of-memory error when trying to download a
very large data matrix (in my tests hundreds of gigabytes worth).

This patch uses several strategies to increase the download size at which
that happens:

- Gets access windows one row at a time and only for the range of
  requested columns. This minimizes tile cache memory needed.
- Converts rows to tsv format on the fly, so we don't need to convert
  the entire matrix to tsv format at once.
- Constructs a blob using a vector of the row tsv data.  This is more
  memory efficient than manually building a data URL.

These steps help but don't eliminate the problem.  (I don't think that's
possible purely in browser.)  A future patch will a display warning notice to
the user for very large download sizes.
  • Loading branch information
bmbroom committed Jun 13, 2023
1 parent 34f5e06 commit 7b9121d
Showing 1 changed file with 58 additions and 46 deletions.
104 changes: 58 additions & 46 deletions NGCHM/WebContent/javascript/Linkout.js
Original file line number Diff line number Diff line change
Expand Up @@ -387,39 +387,44 @@ var linkoutsVersion = 'undefined';
}
}

function createMatrixData (heatMap, selection) {
//console.log ({ m: 'LNK.createMatrixData', selection});
const win = heatMap.getNewAccessWindow({
layer: heatMap.getCurrentDL(),
level: MAPREP.DETAIL_LEVEL,
firstRow: 1,
firstCol: 1,
numRows: heatMap.getNumRows(MAPREP.DETAIL_LEVEL),
numCols: heatMap.getNumColumns(MAPREP.DETAIL_LEVEL),
});
win.onready((win) => {
createMatrixDataTsv(heatMap, win, selection);
});
};

//This function creates a two dimensional array which contains all of the row and
//column labels along with the data for a given selection
function createMatrixDataTsv (heatMap, accessWindow, selection) {

function createMatrixData (heatMap, selection) {
const { labels: rowLabels, items: rowItems } = deGap (selection.rowLabels, selection.rowItems);
const { labels: colLabels, items: colItems } = deGap (selection.colLabels, selection.colItems);
const minCol = Math.min.apply (null, colItems);
const numCols = Math.max.apply (null, colItems) - minCol + 1;

const matrix = new Array();
// Push column headers: empty field followed by column labels.
matrix.push ([""].concat (colLabels));
// Push rows: row label followed by values for each column of that row.
for (let row = 0; row < rowLabels.length; row++) {
const rowItem = rowItems[row];
const rowValues = colItems.map (colItem => accessWindow.getValue (rowItem, colItem));
matrix.push ([rowLabels[row]].concat (rowValues));
matrix.push ([""].concat(colLabels).join('\t')+'\n');

let accessWindow = null; // Hold onto accessWindow until next one created to ensure tiles stay in cache.
processRow(0);

function processRow (row) {
if (row >= rowLabels.length) {
// All requested rows processed. Make matrix available for download.
downloadSelectedData (heatMap, matrix, "Matrix");
} else {
const rowItem = rowItems[row];
// Get access window for this row and the columns requested.
accessWindow = heatMap.getNewAccessWindow({
layer: heatMap.getCurrentDL(),
level: MAPREP.DETAIL_LEVEL,
firstRow: rowItem,
firstCol: minCol,
numRows: 1,
numCols: numCols,
});
accessWindow.onready((win) => {
const rowValues = colItems.map (colItem => win.getValue (rowItem, colItem));
matrix.push ([rowLabels[row]].concat(rowValues).join('\t') + '\n');
processRow(row+1);
});
}
}
// Make matrix available for download.
downloadSelectedData (heatMap, matrix, "Matrix");


// Helper function:
// Remove gaps from labels and items.
Expand Down Expand Up @@ -878,7 +883,8 @@ var linkoutsVersion = 'undefined';
for (let i = 0; i < axisLabels.length; i++) {
covarData.push ([axisLabels[i]].concat(labels.map(lbl => classBars[lbl].values[i])));
}
downloadSelectedData (heatMap, covarData, covarAxis);
const rows = covarData.map (row => row.join('\t') + '\n');
downloadSelectedData (heatMap, rows, covarAxis);
}

function downloadPartialClassBar (labels, covarAxis) {
Expand All @@ -892,7 +898,8 @@ var linkoutsVersion = 'undefined';
for (let i = 0; i < axisLabels.length; i++) {
covarData.push ([axisLabels[i]].concat(labels.map(lbl => classBars[lbl].values[labelIndex[i]-1])));
}
downloadSelectedData (heatMap, covarData, covarAxis);
const rows = covarData.map (row => row.join('\t') + '\n');
downloadSelectedData (heatMap, rows, covarAxis);
}

LNK.copySelectionToClipboard = function(labels,axis){
Expand Down Expand Up @@ -1007,29 +1014,34 @@ var linkoutsVersion = 'undefined';
}
}

// Data is a matrix: an array of arrays.
// Each element of data is a row.
// Each element of a row is a cell.
// Rows is an array of tab-separated row data.
// The first row should be column labels.
// The first cell in each row should be a row label.
function downloadSelectedData (heatMap, data, axis) {
let dataStr = "";
for (let i = 0; i < data.length; i++) {
const rowData = data[i].join('\t');
dataStr += rowData+"\n";
}
const fileName = heatMap.getMapInformation().name + "_" + axis + "_Data.tsv";
download (fileName, dataStr);
// The first field in each row should be a row label.
function downloadSelectedData (heatMap, rows, axis) {
try {
const fileName = heatMap.getMapInformation().name + "_" + axis + "_Data.tsv";
download (fileName, rows);
} catch (error) {
console.error ('Matrix download is too large');
}
}

function download(filename, text) {
var element = document.createElement('a');
element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text));
element.setAttribute('download', filename);
element.style.display = 'none';
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
const blob = new Blob (text, { type: 'text/plain' });
const reader = new FileReader();
reader.onerror = function (e) {
console.error ('Failed to convert to data URL', e, reader);
};
reader.onload = function (e) {
const element = document.createElement('a');
element.setAttribute('href', reader.result);
element.setAttribute('download', filename);
element.style.display = 'none';
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
};
reader.readAsDataURL (blob);
}

LNK.switchPaneToLinkouts = function switchPaneToLinkouts (loc) {
Expand Down

0 comments on commit 7b9121d

Please sign in to comment.