1

We are running a simple PoC with LiteDB which is reading / writing / updating a local database concurrently.

This causes an infinite and very fast memory leak attributable to 8Kb LiteDb.Engine.PageBuffer objects.

Doing these operations sequentially rather than asynchronously does not cause a memory leak.

Does anybody have any ideas here? It doesn't follow that we would need to implement a queue for all DB operations to avoid a memory leak.

LEAKING CODE (asynchronous operations):

public class TestTable1
{ 
    public Guid Id { get; set; }
    public string Property1 { get; set; }
    public int Property2 { get; set; }
}

public class Tester
{
    private System.Timers.Timer _timerWrite;
    private System.Timers.Timer _timerUpdate;
    private System.Timers.Timer _timerRead;


    private LiteDatabase _db;

    public void Start()
    {
        var connectionString = new ConnectionString() { Filename = @"D:\temp\_db", Connection = ConnectionType.Direct, Upgrade = true };
        _db = new LiteDatabase(connectionString);

        _timerWrite = new System.Timers.Timer { Interval = 1000 };
        _timerWrite.Elapsed += _timerWrite_Elapsed;
        _timerWrite.Start();

        _timerRead = new System.Timers.Timer { Interval = 500 };
        _timerRead.Elapsed += _timerRead_Elapsed;
        _timerRead.Start();

        _timerUpdate = new System.Timers.Timer { Interval = 100 };
        _timerUpdate.Elapsed += _timerUpdate_Elapsed;
        _timerUpdate.Start();

    }

    private void _timerUpdate_Elapsed(object sender, ElapsedEventArgs e)
    {
        var table = _db.GetCollection<TestTable1>();
        _timerUpdate.Stop();
        var i = 0;
        var rows = table.Find(x => x.Property2 > 25);
        foreach (var row in rows)
        {
            row.Property1 += "updated";
            table.Update(row);
            i++;
        }

        Console.WriteLine($"Updated {i} rows from table ...");
        _timerUpdate.Start();
    }

    private void _timerRead_Elapsed(object sender, ElapsedEventArgs e)
    {
        var table = _db.GetCollection<TestTable1>();
        _timerRead.Stop();
        var rows = table.Find(x => x.Property2 > 25);

        Console.WriteLine($"Read {rows.Count()} rows from table ...");
        _timerRead.Start();
    }

    private void _timerWrite_Elapsed(object sender, ElapsedEventArgs e)
    {
        var table = _db.GetCollection<TestTable1>();
        _timerWrite.Stop();
        for (var i = 0; i < 100; i++)
        {
            table.Insert(new TestTable1() { Id = Guid.NewGuid(), Property1 = i.ToString(), Property2 = i });
        }

        Console.WriteLine("Added 100 new rows to collection");
        _timerWrite.Start();
    }

    public void Stop()
    {
        _timerUpdate.Elapsed -= _timerUpdate_Elapsed; _timerRead.Elapsed -= _timerRead_Elapsed; _timerWrite.Elapsed -= _timerWrite_Elapsed;
        _timerWrite.Stop(); _timerRead.Stop(); _timerUpdate.Stop();
        _timerUpdate?.Dispose(); _timerRead?.Dispose(); _timerWrite?.Dispose();

        _db.Dispose();
    }
}

NOT LEAKING CODE (sequential operations)

public class Tester
{
    private System.Timers.Timer _timerAll;


    private LiteDatabase _db;

    public void Start()
    {
        var connectionString = new ConnectionString() { Filename = @"D:\temp\_db", Connection = ConnectionType.Direct, Upgrade = true };
        _db = new LiteDatabase(connectionString);

        _timerAll = new System.Timers.Timer { Interval = 100 };
        _timerAll.Elapsed += _timerAll_Elapsed;
        _timerAll.Start();
    }

    private void _timerAll_Elapsed(object sender, ElapsedEventArgs e)
    {
        // update
        var table = _db.GetCollection<TestTable1>();
        _timerAll.Stop();
        var i = 0;
        var rows = table.Find(x => x.Property2 > 25);
        foreach (var row in rows)
        {
            row.Property1 += "updated";
            table.Update(row);
            i++;
        }
        Console.WriteLine($"Updated {i} rows from table ...");

        // read
        rows = table.Find(x => x.Property2 > 25);

        // write            
        for (i = 0; i < 100; i++)
        {
            table.Insert(new TestTable1() { Id = Guid.NewGuid(), Property1 = i.ToString(), Property2 = i });
        }
        Console.WriteLine("Added 100 new rows to collection");

        _timerAll.Start();

    }

    public void Stop()
    {
        _timerAll.Elapsed -= _timerAll_Elapsed;
        _timerAll.Stop();
        _timerAll?.Dispose();
    }
}
James Harcourt
  • 6,017
  • 4
  • 22
  • 42
  • your code looks both fine, there's just one difference. In leaking code there is one more line which is not in non-leaking code. You call `Console.WriteLine($"Read {rows.Count()} rows from table ...");` and probably calling `Count()` method might cause this. Try to remove it from leaking or add it to non-leaking code to see the difference. – michal.jakubeczy Jun 17 '21 at 08:51
  • @michal.jakubeczy as expected that line makes no difference we added it to the non leaking code and it was still not leaking. This is more likely to be related to the way the liteDB collections are accessed, hopefully someone will have an answer. – James Harcourt Jun 17 '21 at 13:55

0 Answers0