From 07393109d1bb34f09e8dc90e4ad454028c2c3549 Mon Sep 17 00:00:00 2001 From: Konstantin Mamaev Date: Wed, 21 Jun 2017 15:58:34 +0300 Subject: [PATCH] Fix canvas context leaks (#10) * Update packages * Update API version * get canvas context once --- .vscode/settings.json | 4 +-- CHANGELOG.md | 4 +++ package.json | 32 +++++++++---------- pbiviz.json | 4 +-- src/columns.ts | 15 +++++---- src/utils.ts | 16 ---------- src/visual.ts | 68 +++++++++++++---------------------------- test/_references.ts | 6 +--- test/helpers/helpers.ts | 9 ------ test/visualTest.ts | 43 +++++++++----------------- tsconfig.json | 2 +- 11 files changed, 70 insertions(+), 133 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 47c1837..8015669 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -19,13 +19,13 @@ "fileMatch": [ "/pbiviz.json" ], - "url": "./.api/v1.5.0/schema.pbiviz.json" + "url": "./.api/v1.7.0/schema.pbiviz.json" }, { "fileMatch": [ "/capabilities.json" ], - "url": "./.api/v1.5.0/schema.capabilities.json" + "url": "./.api/v1.7.0/schema.capabilities.json" } ] } diff --git a/CHANGELOG.md b/CHANGELOG.md index 147f78e..e782864 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.2.12 +* FIX. memory leak: Failed to execute 'getImageData' on 'CanvasRenderingContext2D': Out of memory at ImageData creation +* UPDATE. Updated package dependencies + ## 1.2.11 * FIX. Stop word doesn't work if Word-breaking turned off * FIX. Visual always was removing special characters. Added "Special characters" boolean property to "General" tab which will be control removing spesial characters \ No newline at end of file diff --git a/package.json b/package.json index 3fe2b79..39a4b51 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "powerbi-visuals-wordcloud", - "version": "1.2.11", + "version": "1.2.12", "description": "Word Cloud is a visual representation of word frequency and value. Use it to get instant insight into the most important terms in a set.", "repository": { "type": "git", @@ -10,7 +10,7 @@ "powerbi-visuals" ], "scripts": { - "postinstall": "pbiviz update 1.5.0", + "postinstall": "pbiviz update 1.7.0", "pbiviz": "pbiviz", "start": "pbiviz start", "package": "pbiviz package", @@ -29,30 +29,30 @@ "homepage": "https://github.com/Microsoft/PowerBI-visuals-WordCloud#readme", "dependencies": { "d3": "3.5.5", - "lodash": "4.16.2", - "powerbi-visuals-utils-colorutils": "0.2.1", - "powerbi-visuals-utils-dataviewutils": "1.0.1", - "powerbi-visuals-utils-formattingutils": "0.2.2", - "powerbi-visuals-utils-svgutils": "0.2.1", - "powerbi-visuals-utils-typeutils": "0.2.1" + "lodash": "4.17.4", + "powerbi-visuals-utils-colorutils": "^1.0.0", + "powerbi-visuals-utils-dataviewutils": "^1.0.0", + "powerbi-visuals-utils-formattingutils": "^1.0.0", + "powerbi-visuals-utils-svgutils": "^1.0.0", + "powerbi-visuals-utils-typeutils": "^1.0.0" }, "devDependencies": { "@types/d3": "3.5.36", "@types/jasmine": "2.5.43", "@types/jasmine-jquery": "1.5.29", - "@types/jquery": "2.0.41", + "@types/jquery": "2.0.46", "@types/lodash": "4.14.55", - "jasmine": "2.5.3", + "jasmine": "2.6.0", "jasmine-jquery": "2.1.1", - "karma": "1.5.0", - "karma-chrome-launcher": "2.0.0", + "karma": "1.7.0", + "karma-chrome-launcher": "2.1.1", "karma-jasmine": "1.1.0", "karma-sourcemap-loader": "0.3.7", "karma-typescript-preprocessor": "0.3.1", - "powerbi-visuals-tools": "1.5.0", - "powerbi-visuals-utils-testutils": "^0.2.2", - "tslint": "4.5.1", - "tslint-microsoft-contrib": "4.0.0", + "powerbi-visuals-tools": "1.7.0", + "powerbi-visuals-utils-testutils": "^1.0.0", + "tslint": "5.4.3", + "tslint-microsoft-contrib": "5.0.0", "typescript": "2.1.4" } } diff --git a/pbiviz.json b/pbiviz.json index 42fb5c0..2ec6963 100644 --- a/pbiviz.json +++ b/pbiviz.json @@ -4,12 +4,12 @@ "displayName": "WordCloud", "guid": "WordCloud1447959067750", "visualClassName": "WordCloud", - "version": "1.2.11", + "version": "1.2.12", "description": "Word Cloud is a visual representation of word frequency and value. Use it to get instant insight into the most important terms in a set.", "supportUrl": "http://community.powerbi.com", "gitHubUrl": "https://github.com/Microsoft/PowerBI-visuals-WordCloud" }, - "apiVersion": "1.5.0", + "apiVersion": "1.7.0", "author": { "name": "Microsoft", "email": "pbicvsupport@microsoft.com" diff --git a/src/columns.ts b/src/columns.ts index a8c6368..cd57378 100644 --- a/src/columns.ts +++ b/src/columns.ts @@ -28,24 +28,23 @@ module powerbi.extensibility.visual { // powerbi import DataView = powerbi.DataView; import DataViewValueColumns = powerbi.DataViewValueColumns; - import DataViewCategoricalColumn = powerbi.DataViewCategoricalColumn; import DataViewCategoryColumn = powerbi.DataViewCategoryColumn; import DataViewCategorical = powerbi.DataViewCategorical; import PrimitiveValue = powerbi.PrimitiveValue; import DataViewValueColumn = powerbi.DataViewValueColumn; export class WordCloudColumns { - public static getCategoricalValues(dataView: DataView): WordCloudColumns { + public static getCategoricalValues(dataView: DataView): WordCloudColumns { let categorical: DataViewCategorical = dataView && dataView.categorical, categories: DataViewCategoryColumn[] = categorical && categorical.categories || [], values: DataViewValueColumns = categorical && categorical.values || [] as DataViewValueColumns, series: PrimitiveValue[] = categorical && values.source && this.getSeriesValues(dataView); - return categorical && _.mapValues((new this() as any), (n: any, key: string) => { - return (_.toArray(categories) as DataViewCategoricalColumn[]) - .concat(_.toArray(values)) - .filter((column: DataViewCategoricalColumn) => column.source.roles && column.source.roles[key]) - .map((column: DataViewCategoricalColumn) => column.values)[0] + return categorical && _.mapValues((new this() as any), (n: any, key: string) => { + return (_.toArray(categories)) + .concat(_.toArray(values)) + .filter((column: DataViewCategoryColumn) => column.source.roles && column.source.roles[key]) + .map((column: DataViewCategoryColumn) => column.values)[0] || values.source && values.source.roles && values.source.roles[key] @@ -64,7 +63,7 @@ module powerbi.extensibility.visual { }); } - public static getCategoricalColumns(dataView: DataView): WordCloudColumns { + public static getCategoricalColumns(dataView: DataView): WordCloudColumns { let categorical: DataViewCategorical = dataView && dataView.categorical, categories: DataViewCategoryColumn[] = categorical && categorical.categories || [], values: DataViewValueColumns = categorical && categorical.values || [] as DataViewValueColumns; diff --git a/src/utils.ts b/src/utils.ts index eb8d394..3114898 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -26,22 +26,6 @@ module powerbi.extensibility.visual { export module wordCloudUtils { - export function hexToRgb(hex: string): string { - // Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF") - let shorthandRegex: RegExp = /^#?([a-f\d])([a-f\d])([a-f\d])$/i, - result: RegExpExecArray; - - hex = hex.replace(shorthandRegex, (m: any, r: string, g: string, b: string) => { - return r + r + g + g + b + b; - }); - - result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); - - return result - ? `rgb(${parseInt(result[1], 16)},${parseInt(result[2], 16)},${parseInt(result[3], 16)})` - : null; - } - export function getRandomColor(): string { const red: number = Math.floor(Math.random() * 255), green: number = Math.floor(Math.random() * 255), diff --git a/src/visual.ts b/src/visual.ts index c15625c..1dafd59 100644 --- a/src/visual.ts +++ b/src/visual.ts @@ -76,7 +76,7 @@ module powerbi.extensibility.visual { logn, sqrt, value - }; + } export class WordCloud implements IVisual { private static ClassName: string = "wordCloud"; @@ -139,14 +139,9 @@ module powerbi.extensibility.visual { private static TheFourthLineHeight: string = PixelConverter.toString(15); // Note: This construction fixes bug #6343. private static DefaultTextFontSize: string = PixelConverter.toString(1); - private static MinFakeSize: number = 1; - private static DefaultStrokeStyle: string = "red"; private static DefaultTextAlign: string = "center"; - - private static DefaultCanvasSize: number = 1; - private static ArchimedeanFactor: number = 0.1; private static WidthOffset: number = 5; @@ -220,33 +215,22 @@ module powerbi.extensibility.visual { private wordsContainerSelection: Selection; private wordsGroupUpdateSelection: UpdateSelection; private wordsTextUpdateSelection: UpdateSelection; - - /** - * Public for testability. - */ - public canvas: HTMLCanvasElement; - + public canvasContext: CanvasRenderingContext2D; private fontFamily: string; - private layout: VisualLayout; - private visualHost: IVisualHost; private selectionManager: ValueSelectionManager; - private visualUpdateOptions: VisualUpdateOptions; - private isUpdating: boolean = false; private incomingUpdateOptions: VisualUpdateOptions; - private oldIdentityKeys: string[]; - public static converter( dataView: DataView, colors: IColorPalette, visualHost: IVisualHost, previousData: WordCloudData): WordCloudData { - let categorical: WordCloudColumns, + let categorical: WordCloudColumns, catValues: WordCloudColumns, settings: WordCloudSettings, colorHelper: ColorHelper, @@ -296,12 +280,11 @@ module powerbi.extensibility.visual { let color: string; let selectionIdBuilder: ISelectionIdBuilder; if (categorical.Category.objects && categorical.Category.objects[index]) { - color = wordCloudUtils.hexToRgb(colorHelper.getColorForMeasure( - categorical.Category.objects[index], "")); + color = colorHelper.getColorForMeasure(categorical.Category.objects[index], ""); } else { color = previousData && previousData.texts && previousData.texts[index] ? previousData.texts[index].color - : wordCloudUtils.getRandomColor(); + : colors.getColor(index.toString()).value; } selectionIdBuilder = visualHost.createSelectionIdBuilder() @@ -626,15 +609,17 @@ module powerbi.extensibility.visual { this.clearSelection(); }); - this.fontFamily = this.root.style("font-family"); // TODO: check it. + this.fontFamily = this.root.style("font-family"); this.main = this.root.append("g"); this.wordsContainerSelection = this.main .append("g") - .classed(WordCloud.Words.class, true); + .classed(WordCloud.Words.className, true); - this.canvas = document.createElement("canvas"); + // init canvas context for calculate label positions + const canvas = document.createElement("canvas"); + this.canvasContext = this.getCanvasContext(canvas); } public update(visualUpdateOptions: VisualUpdateOptions): void { @@ -686,8 +671,8 @@ module powerbi.extensibility.visual { private clear(): void { this.main - .select(WordCloud.Words.selector) - .selectAll(WordCloud.WordGroup.selector) + .select(WordCloud.Words.selectorName) + .selectAll(WordCloud.WordGroup.selectorName) .remove(); } @@ -704,8 +689,7 @@ module powerbi.extensibility.visual { let surface: number[] = _.range( WordCloud.MinViewport.width, (this.specialViewport.width >> WordCloud.WidthOffset) * this.specialViewport.height, - WordCloud.MinViewport.width), - canvasContext: CanvasRenderingContext2D; + WordCloud.MinViewport.width); words.forEach((dataPoint: WordCloudDataPoint) => { dataPoint.getWidthOfWord = () => @@ -718,12 +702,10 @@ module powerbi.extensibility.visual { }) + WordCloud.AdditionalTextWidth); }); - canvasContext = this.getCanvasContext(); - - if (canvasContext) { + if (this.canvasContext) { this.computeCycle( words, - canvasContext, + this.canvasContext, surface, null, onPositionsComputed); @@ -1140,20 +1122,15 @@ module powerbi.extensibility.visual { * * Public for testability. */ - public getCanvasContext(): CanvasRenderingContext2D { - if (!this.canvasViewport || !this.canvas) { + public getCanvasContext(canvasElement: HTMLCanvasElement): CanvasRenderingContext2D { + if (!canvasElement) { return null; } - this.canvas.width = WordCloud.DefaultCanvasSize; - this.canvas.height = WordCloud.DefaultCanvasSize; - - let context: CanvasRenderingContext2D = this.canvas.getContext("2d"); - - this.canvas.width = this.canvasViewport.width << WordCloud.WidthOffset; - this.canvas.height = this.canvasViewport.height; + canvasElement.width = this.canvasViewport.width << WordCloud.WidthOffset; + canvasElement.height = this.canvasViewport.height; - context = this.canvas.getContext("2d"); + const context = canvasElement.getContext("2d"); context.fillStyle = context.strokeStyle = WordCloud.DefaultStrokeStyle; context.textAlign = WordCloud.DefaultTextAlign; @@ -1195,14 +1172,14 @@ module powerbi.extensibility.visual { this.scaleMainView(wordCloudDataView); this.wordsGroupUpdateSelection = this.main - .select(WordCloud.Words.selector) + .select(WordCloud.Words.selectorName) .selectAll("g") .data(wordCloudDataView.data); let wordGroupEnterSelection: Selection = this.wordsGroupUpdateSelection .enter() .append("svg:g") - .classed(WordCloud.WordGroup.class, true); + .classed(WordCloud.WordGroup.className, true); wordGroupEnterSelection .append("svg:text") @@ -1486,7 +1463,6 @@ module powerbi.extensibility.visual { public destroy(): void { this.root = null; - this.canvas = null; } } } diff --git a/test/_references.ts b/test/_references.ts index cef00bf..bc684e2 100644 --- a/test/_references.ts +++ b/test/_references.ts @@ -24,12 +24,8 @@ * THE SOFTWARE. */ -// External -/// -/// - // Power BI API -/// +/// // Power BI Extensibility /// diff --git a/test/helpers/helpers.ts b/test/helpers/helpers.ts index b440386..7e98f47 100644 --- a/test/helpers/helpers.ts +++ b/test/helpers/helpers.ts @@ -39,15 +39,6 @@ module powerbi.extensibility.visual.test.helpers { return { solid: { color } }; } - export function areColorsEqual(firstColor: string, secondColor: string): boolean { - const firstConvertedColor: RgbColor = parseColorString(firstColor), - secondConvertedColor: RgbColor = parseColorString(secondColor); - - return firstConvertedColor.R === secondConvertedColor.R - && firstConvertedColor.G === secondConvertedColor.G - && firstConvertedColor.B === secondConvertedColor.B; - } - export function getRandomUniqueIntegers(count: number, min: number = 0, max: number): number[] { const result: number[] = []; diff --git a/test/visualTest.ts b/test/visualTest.ts index 35a2444..b74d1ac 100644 --- a/test/visualTest.ts +++ b/test/visualTest.ts @@ -30,11 +30,11 @@ module powerbi.extensibility.visual.test { // powerbi.extensibility.utils.test import renderTimeout = powerbi.extensibility.utils.test.helpers.renderTimeout; import MockISelectionManager = powerbi.extensibility.utils.test.mocks.MockISelectionManager; + import createColorPalette = powerbi.extensibility.utils.test.mocks.createColorPalette; // powerbi.extensibility.visual.test import WordCloudData = powerbi.extensibility.visual.test.WordCloudData; import WordCloudBuilder = powerbi.extensibility.visual.test.WordCloudBuilder; - import areColorsEqual = powerbi.extensibility.visual.test.helpers.areColorsEqual; import getRandomUniqueHexColors = powerbi.extensibility.visual.test.helpers.getRandomUniqueHexColors; import getSolidColorStructuralObject = powerbi.extensibility.visual.test.helpers.getSolidColorStructuralObject; @@ -68,7 +68,7 @@ module powerbi.extensibility.visual.test { return $.grep(elements, (element: Element) => { return element.innerHTML === "" || element.textContent === text; }); - }; + } describe("DOM tests", () => { it("svg element created", () => { @@ -247,8 +247,9 @@ module powerbi.extensibility.visual.test { describe("Format settings test", () => { describe("Data color", () => { it("colors", (done) => { - let category: DataViewCategoryColumn, - colors: string[]; + const mockColorPallete: powerbi.extensibility.IColorPalette = createColorPalette(); + let category: DataViewCategoryColumn; + let colors: string[] = []; defaultDataViewBuilder .valuesCategoryValues @@ -258,25 +259,25 @@ module powerbi.extensibility.visual.test { category = dataView.categorical.categories[0]; - colors = getRandomUniqueHexColors(category.values.length); - category.objects = category.objects || []; - category.values.forEach((value: PrimitiveValue, index: number) => + category.values.forEach((value: PrimitiveValue, index: number) => { + const color: IColorInfo = mockColorPallete.getColor(index.toString()); + colors.push(color.value); category.objects[index] = { dataPoint: { - fill: getSolidColorStructuralObject(colors[index]) + fill: color.value } - }); + }; + }); visualBuilder.updateRenderTimeout(dataView, () => { visualBuilder.wordText .toArray() .forEach((element: Element) => { const fillColor: string = $(element).css("fill"); - expect(colors.some((color: string) => { - return areColorsEqual(fillColor, color); + return fillColor === color; })); }); @@ -326,7 +327,7 @@ module powerbi.extensibility.visual.test { (dataView.metadata.objects as any).stopWords.words = ""; visualBuilder.updateRenderTimeout(dataView, () => { - expect(grep(visualBuilder.wordText.toArray()).length) + expect(visualBuilder.wordText.toArray().length) .toBeGreaterThan(0); (dataView.metadata.objects as any).stopWords.words = "Afghanistan"; @@ -375,22 +376,8 @@ module powerbi.extensibility.visual.test { visualInstance = visualBuilder.instance; }); - it("shouldn't throw any unexpected exceptions if canvas is undefined", () => { - visualInstance.canvas = null; - - expect(() => { - visualInstance.getCanvasContext(); - }).not.toThrow(); - }); - - it("should return null if canvas is undefined", () => { - visualInstance.canvas = null; - - expect(visualInstance.getCanvasContext()).toBeNull(); - }); - it("should return defined value", () => { - let context: CanvasRenderingContext2D = visualInstance.getCanvasContext(); + let context: CanvasRenderingContext2D = visualInstance.canvasContext; expect(context).not.toBeUndefined(); expect(context).not.toBeNull(); @@ -399,7 +386,7 @@ module powerbi.extensibility.visual.test { describe("Selection", () => { it("Check index of the data-point after filtering", () => { - const item: WordCloudText = VisualClass.converter(dataView, null, visualBuilder.visualHost, null) + const item: WordCloudText = VisualClass.converter(dataView, createColorPalette(), visualBuilder.visualHost, null) .texts .find((item: WordCloudText) => item.text === "Angola"); expect(item.index).toBe(5); diff --git a/tsconfig.json b/tsconfig.json index b0740a3..695dfa7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,7 +9,7 @@ "declaration": true }, "files": [ - ".api/v1.5.0/PowerBI-visuals.d.ts", + ".api/v1.7.0/PowerBI-visuals.d.ts", "node_modules/powerbi-visuals-utils-typeutils/lib/index.d.ts", "node_modules/powerbi-visuals-utils-svgutils/lib/index.d.ts", "node_modules/powerbi-visuals-utils-dataviewutils/lib/index.d.ts",