anchorage_layer_simple/data/migrate-from-files.mjs
ncantu 0960e43a45 Optimisation mémoire api-anchorage avec base de données SQLite
**Motivations:**
- Réduction drastique de la consommation mémoire lors des ancrages
- Élimination du chargement de 173k+ UTXOs à chaque requête
- Stabilisation de la mémoire système sous charge élevée (50+ ancrages/minute)

**Root causes:**
- api-anchorage chargeait tous les UTXOs (173k+) via listunspent RPC à chaque ancrage
- Filtrage et tri de 173k+ objets en mémoire pour sélectionner un seul UTXO
- Croissance mémoire de ~16 MB toutes les 12 secondes avec 50 ancrages/minute
- Saturation mémoire système en quelques minutes

**Correctifs:**
- Création du module database.js pour gérer la base de données SQLite partagée
- Remplacement de listunspent RPC par requête SQL directe avec LIMIT 1
- Sélection directe d'un UTXO depuis la DB au lieu de charger/filtrer 173k+ objets
- Marquage des UTXOs comme dépensés dans la DB après utilisation
- Fermeture propre de la base de données lors de l'arrêt

**Evolutions:**
- Utilisation de la base de données SQLite partagée avec signet-dashboard
- Réduction mémoire de 99.999% (173k+ objets → 1 objet par requête)
- Amélioration des performances (requête SQL indexée vs filtrage en mémoire)
- Optimisation mémoire de signet-dashboard (chargement UTXOs seulement si nécessaire)
- Monitoring de lockedUtxos dans api-anchorage pour détecter les fuites
- Nettoyage des intervalles frontend pour éviter les fuites mémoire

**Pages affectées:**
- api-anchorage/src/database.js (nouveau)
- api-anchorage/src/bitcoin-rpc.js
- api-anchorage/src/server.js
- api-anchorage/package.json
- signet-dashboard/src/bitcoin-rpc.js
- signet-dashboard/public/app.js
- features/optimisation-memoire-applications.md (nouveau)
- features/api-anchorage-optimisation-base-donnees.md (nouveau)
2026-01-27 21:12:22 +01:00

222 lines
7.2 KiB
JavaScript

/**
* Script de migration des fichiers texte vers SQLite
*/
import { readFileSync, existsSync } from 'fs';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
import { createRequire } from 'module';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// Utiliser better-sqlite3 depuis signet-dashboard/node_modules
const require = createRequire(join(__dirname, '../signet-dashboard/package.json'));
const Database = require('better-sqlite3');
const dbPath = join(__dirname, 'signet.db');
const db = new Database(dbPath);
// Activer les clés étrangères
db.pragma('foreign_keys = ON');
// Préparer les requêtes d'insertion
const insertUtxo = db.prepare(`
INSERT OR REPLACE INTO utxos
(category, txid, vout, amount, confirmations, is_anchor_change, block_time, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
`);
const insertAnchor = db.prepare(`
INSERT OR REPLACE INTO anchors
(hash, txid, block_height, confirmations, date, updated_at)
VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
`);
const insertFee = db.prepare(`
INSERT OR REPLACE INTO fees
(txid, fee, fee_sats, block_height, block_time, confirmations, change_address, change_amount, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
`);
const insertManyUtxos = db.transaction((utxos) => {
for (const utxo of utxos) {
insertUtxo.run(
utxo.category,
utxo.txid,
utxo.vout,
utxo.amount,
utxo.confirmations,
utxo.is_anchor_change ? 1 : 0,
utxo.block_time || null
);
}
});
const insertManyAnchors = db.transaction((anchors) => {
for (const anchor of anchors) {
insertAnchor.run(
anchor.hash,
anchor.txid,
anchor.block_height,
anchor.confirmations,
anchor.date
);
}
});
const insertManyFees = db.transaction((fees) => {
for (const fee of fees) {
insertFee.run(
fee.txid,
fee.fee,
fee.fee_sats,
fee.block_height,
fee.block_time,
fee.confirmations,
fee.change_address,
fee.change_amount
);
}
});
// Migrer utxo_list.txt
console.log('📦 Migration de utxo_list.txt...');
const utxoListPath = join(__dirname, '../utxo_list.txt');
if (existsSync(utxoListPath)) {
const content = readFileSync(utxoListPath, 'utf8').trim();
const lines = content.split('\n').filter(line => line.trim());
const utxos = [];
for (const line of lines) {
const parts = line.split(';');
if (parts.length >= 6) {
let category, txid, vout, amount, confirmations, isAnchorChange, blockTime;
if (parts.length === 7 && !isNaN(parseFloat(parts[3]))) {
// Nouveau format: category;txid;vout;amount;confirmations;isAnchorChange;blockTime
[category, txid, vout, amount, confirmations, isAnchorChange, blockTime] = parts;
} else if (parts.length >= 6) {
// Ancien format: category;txid;vout;address;amount;confirmations;isAnchorChange
[category, txid, vout, , amount, confirmations] = parts;
isAnchorChange = parts.length > 6 ? parts[6] === 'true' : false;
blockTime = parts.length > 7 ? parseInt(parts[7], 10) || null : null;
}
utxos.push({
category: category.trim(),
txid: txid.trim(),
vout: parseInt(vout, 10),
amount: parseFloat(amount),
confirmations: parseInt(confirmations, 10) || 0,
is_anchor_change: isAnchorChange === 'true' || isAnchorChange === true,
block_time: blockTime ? parseInt(blockTime, 10) : null
});
}
}
insertManyUtxos(utxos);
console.log(`${utxos.length} UTXOs migrés`);
} else {
console.log('⚠️ utxo_list.txt non trouvé');
}
// Migrer hash_list.txt
console.log('📦 Migration de hash_list.txt...');
const hashListPath = join(__dirname, '../hash_list.txt');
if (existsSync(hashListPath)) {
const content = readFileSync(hashListPath, 'utf8').trim();
const lines = content.split('\n').filter(line => line.trim());
const anchors = [];
for (const line of lines) {
const parts = line.split(';');
if (parts.length >= 2) {
const [hash, txid, blockHeight, confirmations, date] = parts;
anchors.push({
hash: hash.trim(),
txid: txid.trim(),
block_height: blockHeight ? parseInt(blockHeight, 10) : null,
confirmations: confirmations ? parseInt(confirmations, 10) : 0,
date: date || new Date().toISOString()
});
}
}
insertManyAnchors(anchors);
console.log(`${anchors.length} ancrages migrés`);
} else {
console.log('⚠️ hash_list.txt non trouvé');
}
// Migrer fees_list.txt
console.log('📦 Migration de fees_list.txt...');
const feesListPath = join(__dirname, '../fees_list.txt');
if (existsSync(feesListPath)) {
const content = readFileSync(feesListPath, 'utf8').trim();
const lines = content.split('\n').filter(line => line.trim());
const fees = [];
for (const line of lines) {
const parts = line.split(';');
// Format: txid;fee;fee_sats;blockHeight;blockTime;confirmations;changeAddress;changeAmount
if (parts.length >= 3) {
const [txid, fee, feeSats, blockHeight, blockTime, confirmations, changeAddress, changeAmount] = parts;
fees.push({
txid: txid.trim(),
fee: parseFloat(fee) || 0,
fee_sats: parseInt(feeSats, 10) || 0,
block_height: blockHeight ? parseInt(blockHeight, 10) : null,
block_time: blockTime ? parseInt(blockTime, 10) : null,
confirmations: confirmations ? parseInt(confirmations, 10) : 0,
change_address: changeAddress || null,
change_amount: changeAmount ? parseFloat(changeAmount) : null
});
}
}
insertManyFees(fees);
console.log(`${fees.length} frais migrés`);
} else {
console.log('⚠️ fees_list.txt non trouvé');
}
// Migrer les caches
console.log('📦 Migration des caches...');
const utxoCachePath = join(__dirname, '../utxo_list_cache.txt');
if (existsSync(utxoCachePath)) {
const cacheContent = readFileSync(utxoCachePath, 'utf8').trim();
const parts = cacheContent.split(';');
if (parts.length >= 2) {
const insertCache = db.prepare('INSERT OR REPLACE INTO cache (key, value) VALUES (?, ?)');
insertCache.run('utxo_list_cache', cacheContent);
console.log('✅ Cache UTXO migré');
}
}
const hashCachePath = join(__dirname, '../hash_list_cache.txt');
if (existsSync(hashCachePath)) {
const cacheContent = readFileSync(hashCachePath, 'utf8').trim();
const parts = cacheContent.split(';');
if (parts.length >= 2) {
const insertCache = db.prepare('INSERT OR REPLACE INTO cache (key, value) VALUES (?, ?)');
insertCache.run('hash_list_cache', cacheContent);
console.log('✅ Cache hash migré');
}
}
console.log('\n✅ Migration terminée avec succès!');
console.log(`📁 Base de données: ${dbPath}`);
// Afficher les statistiques
const utxoCount = db.prepare('SELECT COUNT(*) as count FROM utxos').get();
const anchorCount = db.prepare('SELECT COUNT(*) as count FROM anchors').get();
const feeCount = db.prepare('SELECT COUNT(*) as count FROM fees').get();
console.log('\n📊 Statistiques:');
console.log(` UTXOs: ${utxoCount.count}`);
console.log(` Ancrages: ${anchorCount.count}`);
console.log(` Frais: ${feeCount.count}`);
db.close();