**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)
12 KiB
Migration fichiers texte vers base de données
Date: 2026-01-27 Auteur: Équipe 4NK
Objectif
Évaluer la migration des fichiers texte (utxo_list.txt, hash_list.txt, fees_list.txt) vers une base de données pour améliorer les performances et la maintenabilité.
État actuel
Fichiers texte utilisés
| Fichier | Taille | Lignes | Format |
|---|---|---|---|
utxo_list.txt |
6.3 MB | 68 397 | category;txid;vout;amount;confirmations;isAnchorChange;blockTime |
hash_list.txt |
5.2 MB | 32 718 | hash;txid;blockHeight;confirmations;date |
fees_list.txt |
411 KB | 2 666 | txid;fee;fee_sats;blockHeight;blockTime;confirmations;changeAddress;changeAmount |
| Total | 11.9 MB | 103 781 | - |
Opérations actuelles
- Lecture complète : Chargement de tout le fichier en mémoire
- Parsing ligne par ligne :
split(';')pour chaque ligne - Recherche : Parsing de toutes les lignes (O(n))
- Mise à jour : Réécriture complète du fichier
- Comptage : Parsing de toutes les lignes
Problèmes identifiés
Performance
- Lecture complète nécessaire : Même pour un seul élément, tout le fichier doit être lu
- Pas d'indexation : Recherche linéaire O(n) pour trouver un élément
- Parsing coûteux : Split et parsing de 68k+ lignes à chaque chargement
- Mise à jour lourde : Réécriture complète du fichier pour une seule modification
Maintenabilité
- Pas de validation de schéma : Format libre, erreurs possibles
- Pas de transactions : Risque de corruption en cas d'interruption
- Pas de concurrence : Accès concurrent non sécurisé
- Pas de requêtes complexes : Impossible de faire des JOIN, GROUP BY, etc.
Exemples de lenteur
// Lecture complète de 68k lignes
const content = readFileSync(utxoListPath, 'utf8').trim();
const lines = content.split('\n');
for (const line of lines) {
const parts = line.split(';'); // Parsing de chaque ligne
// ...
}
// Recherche: parsing de toutes les lignes
for (const line of lines) {
if (line.includes('ancrages')) { // Recherche linéaire
// ...
}
}
// Mise à jour: réécriture complète
writeFileSync(outputPath, allLines.join('\n')); // Réécriture de 6.3 MB
Solution proposée: Base de données
Choix de la base de données
Recommandation: SQLite
Avantages:
- Pas de serveur séparé nécessaire (fichier local)
- Très performant pour ce volume de données
- Support SQL complet
- Transactions ACID
- Indexation native
- Faible empreinte mémoire
- Facile à migrer vers PostgreSQL/MySQL si nécessaire
Alternatives:
- PostgreSQL : Si besoin de serveur centralisé, accès réseau
- MySQL/MariaDB : Si déjà utilisé dans l'infrastructure
- IndexedDB : Si besoin côté client (navigateur)
Schéma proposé
Table utxos
CREATE TABLE utxos (
id INTEGER PRIMARY KEY AUTOINCREMENT,
category TEXT NOT NULL, -- 'bloc_rewards', 'ancrages', 'changes'
txid TEXT NOT NULL,
vout INTEGER NOT NULL,
address TEXT,
amount REAL NOT NULL,
confirmations INTEGER DEFAULT 0,
is_anchor_change BOOLEAN DEFAULT FALSE,
block_time INTEGER,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(txid, vout)
);
CREATE INDEX idx_utxos_category ON utxos(category);
CREATE INDEX idx_utxos_txid_vout ON utxos(txid, vout);
CREATE INDEX idx_utxos_confirmations ON utxos(confirmations);
CREATE INDEX idx_utxos_amount ON utxos(amount);
Table anchors (hash_list.txt)
CREATE TABLE anchors (
id INTEGER PRIMARY KEY AUTOINCREMENT,
hash TEXT NOT NULL UNIQUE,
txid TEXT NOT NULL,
block_height INTEGER,
confirmations INTEGER DEFAULT 0,
date TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_anchors_hash ON anchors(hash);
CREATE INDEX idx_anchors_txid ON anchors(txid);
CREATE INDEX idx_anchors_block_height ON anchors(block_height);
Table fees
CREATE TABLE fees (
id INTEGER PRIMARY KEY AUTOINCREMENT,
txid TEXT NOT NULL UNIQUE,
fee REAL NOT NULL,
fee_sats INTEGER NOT NULL,
block_height INTEGER,
block_time INTEGER,
confirmations INTEGER DEFAULT 0,
change_address TEXT,
change_amount REAL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_fees_txid ON fees(txid);
CREATE INDEX idx_fees_block_height ON fees(block_height);
Comparaison des performances
Opérations courantes
1. Chargement de la liste complète
Fichier texte:
// Temps: ~200-500ms pour 68k lignes
const content = readFileSync(path, 'utf8'); // ~50ms
const lines = content.split('\n'); // ~20ms
for (const line of lines) {
const parts = line.split(';'); // ~200-300ms
// ...
}
Base de données:
-- Temps: ~50-100ms avec index
SELECT * FROM utxos;
Gain: 2-5x plus rapide
2. Recherche par catégorie
Fichier texte:
// Temps: ~200-300ms (parsing de toutes les lignes)
const anchors = lines.filter(line => line.startsWith('ancrages;'));
Base de données:
-- Temps: ~5-10ms avec index
SELECT * FROM utxos WHERE category = 'ancrages';
Gain: 20-40x plus rapide
3. Recherche par txid
Fichier texte:
// Temps: ~200-300ms (recherche linéaire)
const utxo = lines.find(line => line.includes(`;${txid};`));
Base de données:
-- Temps: ~1-2ms avec index
SELECT * FROM utxos WHERE txid = ? AND vout = ?;
Gain: 100-200x plus rapide
4. Mise à jour d'un élément
Fichier texte:
// Temps: ~300-500ms (réécriture complète)
// 1. Lire tout le fichier
// 2. Modifier la ligne
// 3. Réécrire tout le fichier
writeFileSync(path, allLines.join('\n'));
Base de données:
-- Temps: ~2-5ms
UPDATE utxos SET confirmations = ? WHERE txid = ? AND vout = ?;
Gain: 60-250x plus rapide
5. Comptage
Fichier texte:
// Temps: ~100-200ms (parsing de toutes les lignes)
const count = lines.filter(line => line.startsWith('ancrages;')).length;
Base de données:
-- Temps: ~1-2ms (pas de parsing, index uniquement)
SELECT COUNT(*) FROM utxos WHERE category = 'ancrages';
Gain: 50-200x plus rapide
6. Requêtes complexes
Fichier texte:
// Impossible ou très lent
// Exemple: UTXOs disponibles pour ancrage avec confirmations >= 6
const available = lines
.filter(line => line.startsWith('ancrages;'))
.map(line => {
const parts = line.split(';');
return { amount: parseFloat(parts[3]), confirmations: parseInt(parts[4]) };
})
.filter(u => u.amount >= 0.00002 && u.confirmations >= 6);
// Temps: ~300-500ms
Base de données:
-- Temps: ~5-10ms
SELECT * FROM utxos
WHERE category = 'ancrages'
AND amount >= 0.00002
AND confirmations >= 6;
Gain: 30-100x plus rapide
Résumé des gains de performance
| Opération | Fichier texte | Base de données | Gain |
|---|---|---|---|
| Chargement complet | 200-500ms | 50-100ms | 2-5x |
| Recherche par catégorie | 200-300ms | 5-10ms | 20-40x |
| Recherche par txid | 200-300ms | 1-2ms | 100-200x |
| Mise à jour | 300-500ms | 2-5ms | 60-250x |
| Comptage | 100-200ms | 1-2ms | 50-200x |
| Requêtes complexes | 300-500ms | 5-10ms | 30-100x |
Avantages supplémentaires
1. Intégrité des données
- Contraintes : UNIQUE, NOT NULL, CHECK
- Transactions : Rollback en cas d'erreur
- Validation : Types de données stricts
2. Accès concurrent
- Verrous : Gestion automatique des accès concurrents
- Isolation : Transactions isolées
- Pas de corruption : Pas de risque de fichier partiellement écrit
3. Requêtes avancées
-- Exemple: Statistiques par catégorie
SELECT
category,
COUNT(*) as count,
SUM(amount) as total_amount,
AVG(amount) as avg_amount
FROM utxos
GROUP BY category;
-- Exemple: Ancrages récents
SELECT a.*, f.fee_sats
FROM anchors a
LEFT JOIN fees f ON a.txid = f.txid
WHERE a.block_height > ?
ORDER BY a.block_height DESC
LIMIT 100;
4. Maintenance
- Backup : Copie simple du fichier SQLite
- Migration : Scripts SQL pour évolutions de schéma
- Monitoring : Requêtes SQL pour diagnostics
Migration
Étape 1: Création de la base de données
// signet-dashboard/src/database.js
import Database from 'better-sqlite3';
const db = new Database('./data/signet.db');
// Créer les tables
db.exec(`
CREATE TABLE IF NOT EXISTS utxos (...);
CREATE TABLE IF NOT EXISTS anchors (...);
CREATE TABLE IF NOT EXISTS fees (...);
`);
Étape 2: Migration des données existantes
// Script de migration unique
async function migrateFromTextFiles() {
// 1. Lire utxo_list.txt
const utxoLines = readFileSync('utxo_list.txt', 'utf8').split('\n');
// 2. Insérer en batch
const insert = db.prepare(`
INSERT INTO utxos (category, txid, vout, amount, confirmations, ...)
VALUES (?, ?, ?, ?, ?, ...)
`);
const insertMany = db.transaction((utxos) => {
for (const utxo of utxos) {
insert.run(...);
}
});
insertMany(parsedUtxos);
// Répéter pour hash_list.txt et fees_list.txt
}
Étape 3: Adaptation du code
// Avant (fichier texte)
async getUtxoList() {
const content = readFileSync('utxo_list.txt', 'utf8');
const lines = content.split('\n');
// Parsing...
}
// Après (base de données)
async getUtxoList() {
const utxos = db.prepare(`
SELECT * FROM utxos
`).all();
return {
blocRewards: utxos.filter(u => u.category === 'bloc_rewards'),
anchors: utxos.filter(u => u.category === 'ancrages'),
changes: utxos.filter(u => u.category === 'changes'),
};
}
Étape 4: Mise à jour incrémentale
// Au lieu de réécrire tout le fichier
async function updateUtxo(txid, vout, updates) {
db.prepare(`
UPDATE utxos
SET confirmations = ?, updated_at = CURRENT_TIMESTAMP
WHERE txid = ? AND vout = ?
`).run(updates.confirmations, txid, vout);
}
Impact attendu
Performance
- Chargement initial : 2-5x plus rapide
- Recherches : 20-200x plus rapide
- Mises à jour : 60-250x plus rapide
- Requêtes complexes : 30-100x plus rapide
Maintenabilité
- Code plus simple : Pas de parsing manuel
- Moins de bugs : Validation automatique
- Évolutivité : Facile d'ajouter de nouvelles tables/colonnes
- Requêtes SQL : Plus expressives que le parsing manuel
Fiabilité
- Pas de corruption : Transactions ACID
- Accès concurrent : Gestion automatique
- Backup : Copie simple du fichier
Recommandations
Priorité haute
- Migrer vers SQLite pour les gains de performance immédiats
- Créer les index pour optimiser les recherches
- Adapter le code pour utiliser la base de données
Priorité moyenne
- Scripts de migration pour les données existantes
- Tests de performance pour valider les gains
- Documentation des nouvelles requêtes SQL
Priorité basse
- Monitoring des performances de la base de données
- Optimisations supplémentaires si nécessaire
- Migration vers PostgreSQL si besoin de serveur centralisé
Conclusion
Oui, une base de données serait significativement plus performante pour ce cas d'usage :
- Gains de performance : 2-200x selon l'opération
- Meilleure maintenabilité : Code plus simple, moins de bugs
- Plus de fonctionnalités : Requêtes complexes, transactions, etc.
- Meilleure fiabilité : Pas de corruption, accès concurrent sécurisé
Recommandation: Migrer vers SQLite pour un gain immédiat avec un effort minimal (pas de serveur à configurer).