Skip to content

Commit

Permalink
Utilities: optimized memory and loading time
Browse files Browse the repository at this point in the history
  • Loading branch information
Luligu committed Feb 18, 2025
1 parent a2e3f2a commit 1a699ec
Show file tree
Hide file tree
Showing 16 changed files with 727 additions and 595 deletions.
5 changes: 3 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,11 @@ matterbridge-hass v. 0.0.8
- [docker]: Added health check directly in the docker image. No need to change configuration of docker compose.
- [platform]: Saving in the storage the selects for faster loading of plugins.
- [icon]: Added matterbridge svg icon (thanks: https://github.com/robvanoostenrijk https://github.com/stuntguy3000).
- [frontend]: Added processUptime.
- [pluginManager]: Refactor PluginManager to optimize memory and load time.
- [frontend]: Frontend v.2.4.2.
- [PluginManager]: Refactor PluginManager to optimize memory and load time.
- [frontend]: Added processUptime.
- [frontend]: Added Share fabrics and Stop sharing to the menu. This allows to pair other controllers without the need to share from the first controller.
- [utils]: Optimized memory and loading time.

### Changed

Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ To run Matterbridge, you need either a [Node.js](https://nodejs.org/en) environm

If you don't have Node.js already install, please use this method to install it on a debian device: https://github.com/nodesource/distributions.
The supported versions of node are 18, 20 and 22. Please install node 22 LTS.
Node 23 is not supported.
Nvm is not a good choice and should not be used for production.

If you don't have Docker already install, please use this method to install it on a debian device: https://docs.docker.com/desktop/setup/install/linux/debian/.
Expand Down
2 changes: 1 addition & 1 deletion src/frontend.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ process.argv = ['node', 'frontend.test.js', '-logger', 'info', '-matterlogger',
import { jest } from '@jest/globals';
import { AnsiLogger, LogLevel, nf, rs, UNDERLINE, UNDERLINEOFF } from 'node-ansi-logger';
import { Matterbridge } from './matterbridge.js';
import { wait, waiter } from './utils/utils.js';
import { wait, waiter } from './utils/export.js';
import WebSocket from 'ws';
import { onOffLight, onOffOutlet, onOffSwitch, temperatureSensor } from './matterbridgeDeviceTypes.js';
import { Identify } from '@matter/main/clusters';
Expand Down
2 changes: 1 addition & 1 deletion src/matterbridge.bridge.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { jest } from '@jest/globals';

import { AnsiLogger, db, LogLevel, rs, UNDERLINE, UNDERLINEOFF } from 'node-ansi-logger';
import { Matterbridge } from './matterbridge.js';
import { waiter } from './utils/utils.js';
import { waiter } from './utils/export.js';
import { Environment, StorageService } from '@matter/main';
import path from 'node:path';
import os from 'node:os';
Expand Down
2 changes: 1 addition & 1 deletion src/matterbridge.childbridge.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { jest } from '@jest/globals';

import { AnsiLogger, db, LogLevel, nf, rs, UNDERLINE, UNDERLINEOFF } from 'node-ansi-logger';
import { Matterbridge } from './matterbridge.js';
import { wait, waiter } from './utils/utils.js';
import { wait, waiter } from './utils/export.js';
import { Environment, StorageService } from '@matter/main';
import path from 'node:path';
import os from 'node:os';
Expand Down
20 changes: 2 additions & 18 deletions src/matterbridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ import { AnsiLogger, TimestampFormat, LogLevel, UNDERLINE, UNDERLINEOFF, YELLOW,
import { NodeStorageManager, NodeStorage } from './storage/export.js';

// Matterbridge
import { getParameter, getIntParameter, hasParameter } from './utils/export.js';
import { logInterfaces, copyDirectory, getNpmPackageVersion, getGlobalNodeModules } from './utils/utils.js';
import { getParameter, getIntParameter, hasParameter, copyDirectory, withTimeout } from './utils/export.js';
import { logInterfaces, getNpmPackageVersion, getGlobalNodeModules } from './utils/network.js';
import { MatterbridgeInformation, RegisteredPlugin, SanitizedExposedFabricInformation, SanitizedSessionInformation, SessionInformation, SystemInformation } from './matterbridgeTypes.js';
import { PluginManager } from './pluginManager.js';
import { DeviceManager } from './deviceManager.js';
Expand Down Expand Up @@ -2232,22 +2232,6 @@ export class Matterbridge extends EventEmitter {
if (!matterServerNode) return;
this.log.notice(`Closing ${matterServerNode.id} server node`);

// Helper function to add a timeout to a promise
const withTimeout = <T>(promise: Promise<T>, ms: number): Promise<T> => {
return new Promise<T>((resolve, reject) => {
const timer = setTimeout(() => reject(new Error('Operation timed out')), ms);
promise
.then((result) => {
clearTimeout(timer); // Prevent memory leak
resolve(result);
})
.catch((error) => {
clearTimeout(timer); // Ensure timeout does not fire if promise rejects first
reject(error);
});
});
};

try {
await withTimeout(matterServerNode.close(), 30000); // 30 seconds timeout to allow slow devices to close gracefully
this.log.info(`Closed ${matterServerNode.id} server node`);
Expand Down
2 changes: 1 addition & 1 deletion src/pluginManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { Matterbridge } from './matterbridge.js';
import { RegisteredPlugin } from './matterbridgeTypes.js';
import { PluginManager } from './pluginManager.js';
import { execSync } from 'node:child_process';
import { waiter } from './utils/utils.js';
import { waiter } from './utils/export.js';
import { promises as fs } from 'node:fs';
import path from 'node:path';
import { DeviceManager } from './deviceManager.js';
Expand Down
66 changes: 66 additions & 0 deletions src/utils/copyDirectory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/**
* This file contains the copyDirectory function.
*
* @file copyDirectory.ts
* @author Luca Liguori
* @date 2025-02-16
* @version 1.0.0
*
* Copyright 2025, 2026, 2027 Luca Liguori.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License. *
*/

// AnsiLogger module
import { AnsiLogger, LogLevel, TimestampFormat } from '../logger/export.js';

/**
* Copies a directory and all its subdirectories and files to a new location.
*
* @param {string} srcDir - The path to the source directory.
* @param {string} destDir - The path to the destination directory.
* @returns {Promise<boolean>} - A promise that resolves when the copy operation is complete or fails for error.
* @throws {Error} - Throws an error if the copy operation fails.
*/
export async function copyDirectory(srcDir: string, destDir: string): Promise<boolean> {
const log = new AnsiLogger({ logName: 'Archive', logTimestampFormat: TimestampFormat.TIME_MILLIS, logLevel: LogLevel.INFO });

const fs = await import('node:fs').then((mod) => mod.promises);
const path = await import('node:path');

log.debug(`copyDirectory: copying directory from ${srcDir} to ${destDir}`);
try {
// Create destination directory if it doesn't exist
await fs.mkdir(destDir, { recursive: true });

// Read contents of the source directory
const entries = await fs.readdir(srcDir, { withFileTypes: true });

for (const entry of entries) {
const srcPath = path.join(srcDir, entry.name);
const destPath = path.join(destDir, entry.name);

if (entry.isDirectory()) {
// Recursive call if entry is a directory
await copyDirectory(srcPath, destPath);
} else if (entry.isFile()) {
// Copy file if entry is a file
await fs.copyFile(srcPath, destPath);
}
}
return true;
} catch (error) {
log.error(`copyDirectory error copying from ${srcDir} to ${destDir}: ${error instanceof Error ? error.message : error}`);
return false;
}
}
119 changes: 119 additions & 0 deletions src/utils/createZip.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/**
* This file contains the createZip function.
*
* @file createZip.ts
* @author Luca Liguori
* @date 2025-02-16
* @version 1.0.0
*
* Copyright 2025, 2026, 2027 Luca Liguori.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License. *
*/

// Archiver module import types
import type { ArchiverError, EntryData } from 'archiver';

// AnsiLogger module
import { AnsiLogger, LogLevel, TimestampFormat } from '../logger/export.js';

/**
* Creates a ZIP archive from the specified source pattern or directory and writes it to the specified output path.
*
* @param {string} outputPath - The path where the output ZIP file will be written.
* @param {string[]} sourcePaths - The source pattern or directory to be zipped (use path.join for sourcePath).
* @returns {Promise<number>} - A promise that resolves to the total number of bytes written to the ZIP file.
*
* @remarks
* This function uses the `archiver` library to create a ZIP archive. It sets the compression level to 9 (maximum compression).
* The function ensures that the output file is properly closed after the archiving process is complete.
* It logs the progress and the total number of bytes written to the console.
*
* This function uses the `glob` library to match files based on the source pattern (internally converted in posix).
*/
export async function createZip(outputPath: string, ...sourcePaths: string[]): Promise<number> {
const log = new AnsiLogger({ logName: 'Archive', logTimestampFormat: TimestampFormat.TIME_MILLIS, logLevel: LogLevel.INFO });

const { default: archiver } = await import('archiver');
const { glob } = await import('glob');
const { createWriteStream, statSync } = await import('node:fs');
const path = await import('node:path');

log.debug(`creating archive ${outputPath} from ${sourcePaths.join(', ')} ...`);

return new Promise((resolve, reject) => {
const output = createWriteStream(outputPath);
const archive = archiver('zip', {
zlib: { level: 9 }, // Set compression level
});

output.on('close', () => {
log.debug(`archive ${outputPath} closed with ${archive.pointer()} total bytes`);
resolve(archive.pointer());
});

output.on('end', () => {
log.debug(`archive ${outputPath} data has been drained ${archive.pointer()} total bytes`);
});

archive.on('error', (error: ArchiverError) => {
log.error(`archive error: ${error.message}`);
reject(error);
});

archive.on('warning', (error: ArchiverError) => {
if (error.code === 'ENOENT') {
log.warn(`archive warning: ${error.message}`);
} else {
log.error(`archive warning: ${error.message}`);
reject(error);
}
});

archive.on('entry', (entry: EntryData) => {
log.debug(`- archive entry: ${entry.name}`);
});

archive.pipe(output);

for (const sourcePath of sourcePaths) {
// Check if the sourcePath is a file or directory
let stats;
try {
stats = statSync(sourcePath);
} catch (error) {
if (sourcePath.includes('*')) {
const files = glob.sync(sourcePath.replace(/\\/g, '/'));
log.debug(`adding files matching glob pattern: ${sourcePath}`);
for (const file of files) {
log.debug(`- glob file: ${file}`);
archive.file(file, { name: file });
}
} else {
log.error(`no files or directory found for pattern ${sourcePath}: ${error}`);
}
continue;
}
if (stats.isFile()) {
log.debug(`adding file: ${sourcePath}`);
archive.file(sourcePath, { name: path.basename(sourcePath) });
} else if (stats.isDirectory()) {
log.debug(`adding directory: ${sourcePath}`);
archive.directory(sourcePath, path.basename(sourcePath));
}
}
// Finalize the archive (i.e., we are done appending files but streams have to finish yet)
log.debug(`finalizing archive ${outputPath}...`);
archive.finalize().catch(reject);
});
}
70 changes: 70 additions & 0 deletions src/utils/deepCopy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/**
* This file contains the deepCopy function.
*
* @file deepCopy.ts
* @author Luca Liguori
* @date 2025-02-16
* @version 1.0.0
*
* Copyright 2025, 2026, 2027 Luca Liguori.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License. *
*/

/**
* Creates a deep copy of the given value.
*
* @template T - The type of the value being copied.
* @param {T} value - The value to be copied.
* @returns {T} - The deep copy of the value.
*/
export function deepCopy<T>(value: T): T {
if (typeof value !== 'object' || value === null) {
// Primitive value (string, number, boolean, bigint, undefined, symbol) or null
return value;
} else if (Array.isArray(value)) {
// Array: Recursively copy each element
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return value.map((item) => deepCopy(item)) as any;
} else if (value instanceof Date) {
// Date objects
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return new Date(value.getTime()) as any;
} else if (value instanceof Map) {
// Maps
const mapCopy = new Map();
value.forEach((val, key) => {
mapCopy.set(key, deepCopy(val));
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return mapCopy as any;
} else if (value instanceof Set) {
// Sets
const setCopy = new Set();
value.forEach((item) => {
setCopy.add(deepCopy(item));
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return setCopy as any;
} else {
// Objects: Create a copy with the same prototype as the original
const proto = Object.getPrototypeOf(value);
const copy = Object.create(proto);
for (const key in value) {
if (Object.prototype.hasOwnProperty.call(value, key)) {
copy[key] = deepCopy(value[key]);
}
}
return copy as T;
}
}
Loading

0 comments on commit 1a699ec

Please sign in to comment.