Main Site Documentation

USBH_Webcam memory leakage?


#1

I am trying out webcam with Cobra II + N18. Result is not that bad. I get some random glitches, but let’s leave them for now. What I am concerned about is very frequent GC. It is firing every 9 seconds and it says “694428 bytes used, 6645240 bytes available”. I am using only 1/10 of available memory. Btw, numbers do not change. The only thing I could think of that could explain this behavior is memory leakage in unmanaged code. I am using simplified version of this code (I don’t use Glide): https://www.ghielectronics.com/glide/example/1/


#2

@ iamin - you are not describing a problem. You might want to see if you can reduce memory allocations. If not, turn off the GC message if it makes you feel better.


#3

@ Mike - I did describe the problem. It is too frequent garbage collection. Under normal conditions, it should not happen. Why garbage collection is being triggered? For no reason? I know I can turn it off, but it would mean I am hiding the problem, not solving it.

This issue could become much more relevant when GC will take more than 200 ms to complete (I have experienced this issue when I was developing GUI). Would you be happy to see your system freeze for 200+ ms every 10 seconds?


#4

@ andre.m - I guess I was expecting one of those answers: “Yes, there is a memory leak, I can reproduce it” or “No, there is no sign of memory leak”.

Here is the code. I don’t think I can optimize anything in this code, but I would be happy to be proven wrong :wink: .

Imports GHI.Premium.System
Imports GHI.Premium.USBHost

Namespace GadgeteerApp1
    Partial Public Class Program
        Private WithEvents _webcam As USBH_Webcam
        Private _webcamFormat As USBH_Webcam.ImageFormat
        Private _webcamThread As Thread

        Public Sub ProgramStarted()
            Debug.Print("Program Started")

            ' Subscribe to USBH events.
            AddHandler USBHostController.DeviceConnectedEvent, AddressOf USBHostController_DeviceConnectedEvent
        End Sub

        Private Sub USBHostController_DeviceConnectedEvent(device As USBH_Device)
            If (device.TYPE = USBH_DeviceType.Webcamera) AndAlso _webcam Is Nothing Then
                _webcam = New USBH_Webcam(device)

                Dim formats() As USBH_Webcam.ImageFormat = _webcam.GetSupportedFormats()

                If formats.Length > 0 Then
                    ' Use the smallest format (usually the last one).
                    _webcamFormat = formats(formats.Length - 1)

                    ' Run a thread to display the video.
                    _webcamThread = New Thread(AddressOf WebcamThread)
                    _webcamThread.Start()
                End If
            End If
        End Sub

        Public Sub WebcamThread()
            Dim myBitmap As New Bitmap(128, 160)
            Dim width, height As Integer

            Try
                _webcam.StartStreaming(_webcamFormat)
                width = _webcam.CurrentFormat.Width
                height = _webcam.CurrentFormat.Height
                Do
                    ' Is there a new image?
                    If _webcam.IsNewImageReady() Then
                        _webcam.DrawImage(myBitmap, 0, 0, width, height)
                        display_N18.Draw(myBitmap)
                    End If
                Loop
            Catch e As Exception
                Debug.Print("Exception: " & e.Message)
            End Try
        End Sub

        Private Sub _webcam_Disconnected(sender As USBH_Webcam, args As USBH_WebcamEventArgs) Handles _webcam.Disconnected
            _webcamThread.Abort()
            _webcam.StopStreaming()
            _webcam = Nothing
            display_N18.Clear()
        End Sub
    End Class
End Namespace

Btw, found this on the netmf forum: "The garbage collector only gets executed automatically when there is not enough memory to allocate certain items or if the firmware vendor set the memory thresholds and they were exceeded."
So I still want to know what exactly happens so critical every 9th second that GC is triggered. There is enough free memory and I don’t think that GHI has set memory threshold to something as low as 1/10 of available memory. So what else could it be?


#5

for jollies why don’t you move your bitmap outside the scope of your thread ?

I can tell you EXACTLY where the memory churn is - in display_N18.Draw(myBitmap)

There’s a bitmap buffer in the driver that gets overwritten/replaced. Very common to do that in a video / display related driver.


#6

I guess because this thread starts only once, so variable is created also only once. What would be an advantage of moving it out?

OK, you are right about this. I have modified this driver: I have made all byte arrays global and I have made them fixed size. I expect that this will make arrays be placed in the same memory region and the size of this region will not fluctuate. In theory it should solve the issue, right? Apparently, it does not ??? .

With the following test I get these results:

[quote]79th: 662280 bytes used, 6677388 bytes available
155th: 661752 bytes used, 6677916 bytes available
231st: 661752 bytes used, 6677916 bytes available
307th: 661908 bytes used, 6677760 bytes available
383rd: 661752 bytes used, 6677916 bytes available[/quote]

This data raises one question:
Does GC show memory usage [em]before[/em] or [em]after[/em] its work is done? I would expect it to be “before”. If so, then GC should not fire automatically because there is no reason for it - there is plenty of free memory.

Program.vb:

Imports Gadgeteer

Namespace GadgeteerApp1
    Partial Public Class Program
        Private WithEvents display_N18 As New Display_N18(6)
        Private WithEvents myTimer As New Timer(1000, Timer.BehaviorType.RunOnce)

        Public Sub ProgramStarted()
            myTimer.Start()
            Debug.Print("Program Started")
        End Sub

        Private Sub myTimer_Tick(timer As Timer) Handles myTimer.Tick
            Dim emptyBMP As New Bitmap(128, 160)

            For i = 1 To 400
                Debug.Print(i.ToString())
                display_N18.Draw(emptyBMP)
            Next
            Debug.Print("DONE!")
        End Sub
    End Class
End Namespace

Display_N18.vb:

Imports GT = Gadgeteer
Imports GTM = Gadgeteer.Modules
Imports GTI = Gadgeteer.Interfaces
Imports Microsoft.SPOT.Hardware

''' <summary>
''' A Display N18 module for Microsoft .NET Gadgeteer
''' </summary>
Public Class Display_N18
    Inherits GTM.Module.DisplayModule

    Private ReadOnly _spi As GTI.SPI
    Private ReadOnly _spiConfig As GTI.SPI.Configuration
    Private ReadOnly _netMfSpiConfig As Spi.Configuration
    Private ReadOnly _socket As GT.Socket
    Private ReadOnly _resetPin As GT.Interfaces.DigitalOutput
    Private ReadOnly _backlightPin As GT.Interfaces.DigitalOutput
    Private ReadOnly _rs As GT.Interfaces.DigitalOutput

    Private ReadOnly _byteArray() As Byte
    Private ReadOnly _shortArray() As UShort

 #Region "Moved out"
    Private ReadOnly _dataEmpty((64 * 80 * 2) - 1) As Byte  'zero-init'd by default
    Private _vram(40959) As Byte                            ' JUST FOR AN EXPERIMENT- REMOVE 40959
    Private _bmpBytes(81919) As Byte                        ' JUST FOR AN EXPERIMENT- REMOVE 81919
 #End Region

    ''' <summary>
    ''' Gets the width of the display.
    ''' </summary>
    ''' <remarks>
    ''' This property always returns 128.
    ''' </remarks>
    Public Overrides ReadOnly Property Width() As UInteger
        Get
            Return 128
        End Get
    End Property

    ''' <summary>
    ''' Gets the height of the display.
    ''' </summary>
    ''' <remarks>
    ''' This property always returns 160.
    ''' </remarks>
    Public Overrides ReadOnly Property Height() As UInteger
        Get
            Return 160
        End Get
    End Property

    ''' <summary>Constructor</summary>
    ''' <param name="socketNumber">The socket that this module is plugged in to.</param>
    Public Sub New(socketNumber As Integer)
        MyBase.New(WPFRenderOptions.Intercept)
        _byteArray = New Byte(0) {}
        _shortArray = New UShort(1) {}

        _socket = GT.Socket.GetSocket(socketNumber, True, Me, Nothing)
        _socket.EnsureTypeIsSupported("S"c, Me)

        _resetPin = New GT.Interfaces.DigitalOutput(_socket, GT.Socket.Pin.Three, False, Me)
        _backlightPin = New GT.Interfaces.DigitalOutput(_socket, GT.Socket.Pin.Four, False, Me)
        _rs = New GT.Interfaces.DigitalOutput(_socket, GT.Socket.Pin.Five, False, Me)

        _spiConfig = New GT.Interfaces.SPI.Configuration(False, 0, 0, False, True, 12000)
        _netMfSpiConfig = New SPI.Configuration(_socket.CpuPins(6), _spiConfig.ChipSelectActiveState, _spiConfig.ChipSelectSetupTime, _spiConfig.ChipSelectHoldTime, _spiConfig.ClockIdleState, _spiConfig.ClockEdge, _spiConfig.ClockRateKHz, _socket.SPIModule)
        _spi = New GT.Interfaces.SPI(_socket, _spiConfig, GT.Interfaces.SPI.Sharing.Shared, _socket, GT.Socket.Pin.Six, Me)

        Reset()

        ConfigureDisplay()

        Clear()

        SetBacklight(True)
    End Sub

    ''' <summary>
    ''' Enables or disables the display backlight.
    ''' </summary>
    ''' <param name="state">The state to set the backlight to.</param>
    Public Sub SetBacklight(state As Boolean)
        _backlightPin.Write(state)
    End Sub

    ''' <summary>
    ''' Clears the display.
    ''' </summary>
    Public Sub Clear()
        DrawRaw(_dataEmpty, 64, 80, 0, 0)
        DrawRaw(_dataEmpty, 64, 80, 64, 0)
        DrawRaw(_dataEmpty, 64, 80, 0, 80)
        DrawRaw(_dataEmpty, 64, 80, 64, 80)
    End Sub

    ''' <summary>
    ''' Draws an image to the screen.
    ''' </summary>
    ''' <param name="bmp">The bitmap to be drawn to the screen</param>
    ''' <param name="x">Starting X position of the image.</param>
    ''' <param name="y">Starting Y position of the image.</param>
    Public Sub Draw(bmp As Bitmap, Optional x As UInteger = 0, Optional y As UInteger = 0)
        'ReDim _vram((bmp.Width * bmp.Height * 2) - 1)              ' JUST FOR AN EXPERIMENT- UNCOMMENT IT
        _bmpBytes = bmp.GetBitmap()
        GT.Modules.Module.Mainboard.NativeBitmapConverter(_bmpBytes, _vram, GT.Mainboard.BPP.BPP16_BGR_BE)
        DrawRaw(_vram, CUInt(bmp.Width), CUInt(bmp.Height), x, y)
    End Sub

    ''' <summary>
    ''' Draws an image to the specified position on the screen.
    ''' </summary>
    ''' <param name="rawData">Raw Bitmap data to be drawn to the screen.</param>
    ''' <param name="x">Starting X position of the image.</param>
    ''' <param name="y">Starting Y position of the image.</param>
    ''' <param name="width">Width of the image.</param>
    ''' <param name="height">Height of the image.</param>
    Public Sub DrawRaw(rawData() As Byte, width As UInteger, height As UInteger, x As UInteger, y As UInteger)
        If (x > Me.Width) OrElse (y > Me.Height) Then Return

        If x + width > Me.Width Then width = Me.Width - x
        If y + height > Me.Height Then height = Me.Height - y

        SetClippingArea(x, y, width - 1UI, height - 1UI)
        WriteCommand(&H2C)
        WriteData(rawData)
    End Sub

    ''' <summary>
    ''' Renders Bitmap data on the display device. 
    ''' </summary>
    ''' <param name="bitmap">The <see cref="T:Microsoft.SPOT.Bitmap"/> object to render on the display.</param>
    Protected Overrides Sub Paint(bitmap As Bitmap)
        Try
            If Mainboard.NativeBitmapCopyToSpi IsNot Nothing Then
                SetClippingArea(0, 0, CUInt(bitmap.Width) - 1UI, CUInt(bitmap.Height) - 1UI)
                WriteCommand(&H2C)
                _rs.Write(True)
                Mainboard.NativeBitmapCopyToSpi(bitmap, _netMfSpiConfig, 0, 0, bitmap.Width, bitmap.Height, GT.Mainboard.BPP.BPP16_BGR_BE)
            Else
                Draw(bitmap)
            End If
        Catch
            ErrorPrint("Painting error")
        End Try
    End Sub

    Private Sub Reset()
        _resetPin.Write(False)
        Thread.Sleep(150)
        _resetPin.Write(True)
    End Sub

    Private Sub ConfigureDisplay()
        Dim lcdConfig As New GT.Mainboard.LCDConfiguration()

        lcdConfig.LCDControllerEnabled = False
        lcdConfig.Width = Width
        lcdConfig.Height = Height

        DisplayModule.SetLCDConfig(lcdConfig)

        WriteCommand(&H11) 'Sleep exit
        Thread.Sleep(120)

        'ST7735R Frame Rate
        WriteCommand(&HB1)
        WriteData(&H1)
        WriteData(&H2C)
        WriteData(&H2D)
        WriteCommand(&HB2)
        WriteData(&H1)
        WriteData(&H2C)
        WriteData(&H2D)
        WriteCommand(&HB3)
        WriteData(&H1)
        WriteData(&H2C)
        WriteData(&H2D)
        WriteData(&H1)
        WriteData(&H2C)
        WriteData(&H2D)

        WriteCommand(&HB4) 'Column inversion
        WriteData(&H7)

        'ST7735R Power Sequence
        WriteCommand(&HC0)
        WriteData(&HA2)
        WriteData(&H2)
        WriteData(&H84)
        WriteCommand(&HC1)
        WriteData(&HC5)
        WriteCommand(&HC2)
        WriteData(&HA)
        WriteData(&H0)
        WriteCommand(&HC3)
        WriteData(&H8A)
        WriteData(&H2A)
        WriteCommand(&HC4)
        WriteData(&H8A)
        WriteData(&HEE)

        WriteCommand(&HC5) 'VCOM
        WriteData(&HE)

        WriteCommand(&H36) 'MX, MY, RGB mode
        WriteData(&HC0)     ' Changed from &HC8 to &HC0

        'ST7735R Gamma Sequence
        WriteCommand(&HE0)
        WriteData(&HF)
        WriteData(&H1A)
        WriteData(&HF)
        WriteData(&H18)
        WriteData(&H2F)
        WriteData(&H28)
        WriteData(&H20)
        WriteData(&H22)
        WriteData(&H1F)
        WriteData(&H1B)
        WriteData(&H23)
        WriteData(&H37)
        WriteData(&H0)

        WriteData(&H7)
        WriteData(&H2)
        WriteData(&H10)
        WriteCommand(&HE1)
        WriteData(&HF)
        WriteData(&H1B)
        WriteData(&HF)
        WriteData(&H17)
        WriteData(&H33)
        WriteData(&H2C)
        WriteData(&H29)
        WriteData(&H2E)
        WriteData(&H30)
        WriteData(&H30)
        WriteData(&H39)
        WriteData(&H3F)
        WriteData(&H0)
        WriteData(&H7)
        WriteData(&H3)
        WriteData(&H10)

        WriteCommand(&H2A)
        WriteData(&H0)
        WriteData(&H0)
        WriteData(&H0)
        WriteData(&H7F)
        WriteCommand(&H2B)
        WriteData(&H0)
        WriteData(&H0)
        WriteData(&H0)
        WriteData(&H9F)

        WriteCommand(&HF0) 'Enable test command
        WriteData(&H1)
        WriteCommand(&HF6) 'Disable ram power save mode
        WriteData(&H0)

        WriteCommand(&H3A) '65k mode
        WriteData(&H5)

        WriteCommand(&H29) 'Display on
    End Sub

    Private Sub SetClippingArea(x As UInteger, y As UInteger, w As UInteger, h As UInteger)
        _shortArray(0) = CUShort(x)
        _shortArray(1) = CUShort(x + w)
        WriteCommand(&H2A)
        WriteData(_shortArray)

        _shortArray(0) = CUShort(y)
        _shortArray(1) = CUShort(y + h)
        WriteCommand(&H2B)
        WriteData(_shortArray)
    End Sub

    Private Sub WriteCommand(command As Byte)
        _byteArray(0) = command

        _rs.Write(False)
        _spi.Write(_byteArray)
    End Sub

    Private Sub WriteData(data As Byte)
        _byteArray(0) = data
        WriteData(_byteArray)
    End Sub

    Private Sub WriteData(data() As Byte)
        _rs.Write(True)
        _spi.Write(data)
    End Sub

    Private Sub WriteData(data() As UShort)
        _rs.Write(True)
        _spi.Write(data)
    End Sub
End Class

#7

The memory shown by GC is after cleanup.

GC is run every 10 seconds, regardless of available memory, to call the destructors of objects with zero reference counts, prior to cleanup. if a lot of objects have been allocated/freed the GC time gets long.

a statement like “i.ToString()” will result in a memory allocation.

In your debug code, the results say it seem to run continuously, but your time creation says to run once?

Running continuously, there are at least 4000 allocations/frees every 10 seconds.

Also, writing to a display 400 times a second is too much. I believe the human eye can only handle about 32 changes per second.


#8

@ Mike - Where did you read that “GC is run every 10 seconds, regardless of available memory”? I have never experienced such a rapid GC before. GC won’t be called every 10 seconds if you run an empty program, it will not be called in this example as well (it runs for about 50 s):

Private Sub myTimer2_Tick(timer As Timer) Handles myTimer2.Tick
	Dim c As Integer
	Dim s As String

	For i2 = 1 To 50
		s = Date.Now.ToString()
		Debug.Print(s)
		For i = 1 To 50000
			c += 1
		Next
	Next
End Sub

I know, but I am not calling it million times, so it should not make any real impact.

Timer is called only once, but there is a loop within timer which paints a black screen 400 times.

Where did you see that? All huge data structures (byte arrays) are fixed size and they are global.

Actually I am writing less than 10 times a second.

And I have just found another strange thing. According to MS, [em]Debug.GC[/em] returns “The amount of free (unused) memory, in bytes”, but actually it returns available memory size.


That line is printing:
[quote]GC: 2msec 662352 bytes used, 6677316 bytes available

...
6677316[/quote]

#9

Sorry, what do you interpret the difference is between “free” and “available” ? They are the same.


#10

@ Brett - To my understanding, Free = Available - Used. And that’s what MS says “free (unused)”.
In my case that wold be Available - Used = 6677316 (bytes available) - 662352 (bytes used) = 6014964 (bytes unused).

Or in other words, I assume [em]Used[/em] + [em]Unused[/em] = [em]Total/Available[/em].
And even simpler example: There are eight computers available, six are currently used by students and two are free, you can use any of these two. 8)


#11

Since destructors are not automatically run when object reference count goes to zero, when are they run?

free == unused == available ?

GC running every 10 seconds and taking 2ms if very common.

GC is complex. For a full understanding, you will have to read the MF code in the porting SDK.


#12

I gave you an example where GC was not triggered for 50 seconds under heavy CPU load. I can give you many more examples where GC won’t be triggered every 10 seconds. I don’t think it is set to be triggered at specified time intervals. As I have quoted before, it is triggered when there is not enough free memory left.


#13

I expect NETMF GC to behave similarly to full framework GC. Let’s try a simple example: in one case I use global array, in another case I create an array every single time until I run out of memory and GC kicks in. In the first case GC won’t run just after [em]MakeBigArray[/em] method is completed. Actually it won’t run at least for 10 mins (have not waited for longer than that).

Print screens show RAM usages for the last 60 seconds, I have 8 GB or RAM.

Module Module1
    Dim _bigArray(1000 * 1000 * 1000) As Byte

    Sub Main()
        GoTo 1

1:
        For i = 1 To 5
            MakeBigArray(i)
        Next
        Console.Read()
        Return

2:
        For i = 1 To 5
            MakeBigArray2(i)
        Next
        Console.Read()
    End Sub

    Private Sub MakeBigArray(val As Byte)
        For i = 0 To _bigArray.Count - 1
            _bigArray(i) = val
        Next
    End Sub

    Private Sub MakeBigArray2(val As Byte)
        Dim bigArray(1000 * 1000 * 1000) As Byte

        For i = 0 To bigArray.Count - 1
            bigArray(i) = val
        Next
    End Sub
End Module

#14

[quote=“iamin”]
I expect NETMF GC to behave similarly to full framework GC. [/quote]
You’re destined to be disappointed.

“Available” is how much is “not used”, not how big the memory is on the device in question.


#15

It is quite possible. Instead of spending my time trying everything out I would much more prefer to read about what is happening behind the scenes with GC. Unfortunately, I have not found any good books, articles etc. on this matter.

Wow, I could bet it is not the case, [em]but it is[/em]! That’s not straightforward at all.
:wall:

Here is one more experiment. In 1 min 21 second I fill 2.5 MB of RAM. During that time automatic GC was not triggered.

Imports Gadgeteer

Namespace FreeMemory
    Partial Public Class Program
        Private WithEvents _myTimer As New Timer(1000, Timer.BehaviorType.RunOnce)
        Private _bigArray1(), _bigArray2(), _bigArray3(), _bigArray4(), _bigArray5() As Byte

        Public Sub ProgramStarted()
            _myTimer.Start()
            Debug.Print("Program Started")
        End Sub

        Private Sub _myTimer_Tick(timer As Timer) Handles _myTimer.Tick
            Debug.Print("START: " & DateTime.Now.ToString())
            Debug.Print("1: " & Debug.GC(False).ToString())
            ReDim _bigArray1(500000)
            'Debug.Print("2: " & Debug.GC(False).ToString())
            For i = 0 To 500000
                _bigArray1(i) = 255
            Next

            'Debug.Print("3: " & Debug.GC(False).ToString())
            ReDim _bigArray2(500000)
            'Debug.Print("4: " & Debug.GC(False).ToString())
            For i = 0 To 500000
                _bigArray2(i) = 255
            Next
            'Debug.Print("5: " & Debug.GC(False).ToString())
            ReDim _bigArray3(500000)
            'Debug.Print("6: " & Debug.GC(False).ToString())
            For i = 0 To 500000
                _bigArray3(i) = 255
            Next
            'Debug.Print("7: " & Debug.GC(False).ToString())
            ReDim _bigArray4(500000)
            'Debug.Print("8: " & Debug.GC(False).ToString())
            For i = 0 To 500000
                _bigArray4(i) = 255
            Next
            'Debug.Print("9: " & Debug.GC(False).ToString())
            ReDim _bigArray5(500000)
            'Debug.Print("10: " & Debug.GC(False).ToString())
            For i = 0 To 500000
                _bigArray5(i) = 255
            Next

            Debug.Print("11: " & Debug.GC(False).ToString())
            Debug.Print("END: " & DateTime.Now.ToString())
        End Sub
    End Class
End Namespace

[quote]START: 06/01/2011 00:20:42
1: 6857628
11: 4361388
END: 06/01/2011 00:22:03[/quote]

[line]

I have also done one more experiment with my last code where I fill LCD with black image. I have modified my code like this:

For i = 1 To 400
	Debug.Print("----------------- GC was called by code -----------------")
	Debug.GC(False)
	Debug.Print("----------------- ///////////////////// -----------------")
	Debug.Print(i.ToString())
	display_N18.Draw(emptyBMP)
Next

Guess what has happened? Automatic GC was never triggered! So, is [em]Debug.GC(False)[/em] doing more than just printing out current memory consumption?
I attach a graph of used memory during that 1 to 400 loop.
The output is here: http://privatepaste.com/e8345aafc5

I guess it is a good time to stop my investigation. I still don’t have a clue why automatic GC was triggering and why it is not triggered when [em]Debug.GC(False)[/em] is used.
I would be happy if anyone who knows what exactly is going on behind the scenes to reply.


#16

BTW, using Debug.Print can cause all sorts of funny things to happen.


#17

As @ Mike said, you need to read the porting kit code to really understand the GC. That’s the only reading that will help you get more info :slight_smile:

Actually, that is the common use of “available” and “free” and “unused” words, so it is straightforward.

debug.gc returns a UINT32. Grab that and watch how memory changes, if at all, through your loop passes. debug.gc(false) simply returns the memory available at any point in time without forcing compaction, so you should simply get an indication of the real memory profile of that code block (and it should not then alter the behaviour of the GC).

Of course, if all you’re worried about is the messages then you can use Debug.EnableGCMessages(false) so you can’t see it :>


#18

[quote=“Brett”]debug.gc returns a UINT32. Grab that and watch how memory changes, if at all, through your loop passes. debug.gc(false) simply returns the memory available at any point in time without forcing compaction, so you should simply get an indication of the real memory profile of that code block (and it should not then alter the behaviour of the GC).
[/quote]
That’s exactly what I did, you can see a graph in my previous post that shows exactly that. As I have noted, when I was using [em]Debug.GC(False)[/em], I was no longer getting automatic GC. If I comment [em]Debug.GC(False)[/em] out, I start seeing automatic GC.


#19

your code never shows that you output the value - so you’re not showing us the code you’re actually testing !


#20

GC is complex. You are assuming calling GC(false) does nothing. But, it could call the destructors of recently freed objects. That could have an impacted on available memory, and postpone compaction.