- status: open --> open-works-for-me
Verbose problem description - skip to end for solution:
Further to the ERangeError reported previously, I was
having trouble with an elusive memory leak when loading
and transforming an XML file using a TXpXSLProcessor,
TXpFilterHTML and TXpObjModel.
At the end of processing, FastMM (a replacement memory
manager, also available on SourceForge) was reporting a
3780 byte leak, and producing a 120MB logfile detailing
the unit-file/s responsible.
From this information I was able to ascertain that the
problem originated inside of XpXmlDom.pas - although
most of my investigation initially focused around
XpDOM.pas.
Here I added a unit-global TList, and on each
TXpNode.Create, I added a reference to the new object
to that list; correspondingly, on every TXpNode.Destroy
the object reference was removed. By using Delphi
CodeInsight, I could inspect the content of this list
at program termination, and observed some 14,500+
unfreed TXpNode's (or descendants - very many of the
XMLPartner classes derive from TXpNode). Further
testing revealed that almost all of these nodes were
children of two TXpDocument objects that existed in the
TList.
To find out where these TXpDocuments were being created
and track their lifespans, I modified the class
definition for TXpDocument, adding a new public Integer
variable (FUID), and a second unit-wide Integer
variable (FUIDCounter) to XpDOM.pas, which was set to
'0' (zero) in the unit Initialization section.
Each time a new TXpDocument was created, its FUID was
set to the current value of FUIDCounter, and then
FUIDCounter was incremented by '1'.
Stepping through XpDOM.pas, I could watch as individual
TXpDocument objects were created and destroyed - and I
spotted an interesting pattern. I could also monitor
*which* of the TXpDocuments weren't being free'd, by
peering into my unit-global TList at program
termination - so I could track which of the
TXpDocuments were still hanging around when the program
was being shutdown.
I use Borland's TXMLDocument in my application, setting
xmldom.DefaultDomVendor to 'sXpXML' at startup:
xmldom.DefaultDomVendor := sXpXML
- so that XMLPartner is used to implement its DOM
interfaces. When TXMLDocument is set to 'Active'
(XMLDoc.pas, at about line 2215 in Delphi6 Update 2),
an IDOMImplementation is first created, which is in
turn used to create a IDOMDocument by calling:
<IDOMImplementation>.createDocument
In XMLPartner, createDocument is implemented in
XpXmlDom.pas on line 462, and returns the expected
IDOMDocument.
The XMLPartner 'IDOMImplementation' contains an
internal object called 'XpDOMImpl' - a
TXpDomImplementation object that includes its own
CreateDocument method: and it's this method that's used
to manufacture the TXpDocument around which the
IDOMDocument interface (that gets passed out of
<IDOMImplementation>.createDocument) wraps - see
XpDOM.pas line 2645.
When the TXpDocument is first created, AddRef is called
to set its reference count to '1' (via 'inherited' -
AddRef is actually called when any TXpNode descendant
is instantiated).
The new TXpDocument object is passed to the constructor
for TXpDOMDocument (XpXmlDom.pas line 1120), where
AddRef is called on it AGAIN via a call to 'inherited'
- so at this point the underlying TXpDocument object
has a reference count of 2.
From here, my application runs its course - lots of
data is added to the TXMLDocument.
When the program ends, and I free the TXMLDocument
('ReleaseDoc' inside of XMLDoc.pas, where the
IDOMDocument it contains (an XMLPartner TXpDOMDocument,
recall) is set to nil:
FDOMDocument := nil;
When this happens, the TXpDocument that the
IDOMDocument interface wraps, SHOULD be free'd. But it
isn't.
In Destructor of the IDOMDocument (TXpDOMDocument),
AddRef is called on the underlying TXpDocument;
immediately afterwards, a call to 'inherited'
decrements the TXpDocument reference count (i.e. one is
added, then taken away). Finally, another object
contained by the IDOMDocument is free'd (a
TXpObjModel), which ALSO contains a reference to the
TXpDocument. When the TXpObjModel is free'd, it
decrements the reference count on the TXpDocument.
To simplify: when the TXpDocument is destroyed, it's
reference count is decremented twice.
TXpDocument objects are created and destroyed frquently
when XMLParner is used to process XML documents. In the
majority of cases, each TXpDocument only has its
reference count incremented two times - so when it
comes to be destroyed, those two calls two Release are
sufficient to drop their overall reference counts to
'0' (zero), and they ARE free'd correctly.
However, when a TXpDocument is created as the result of
a call by an IDOMImplementation to createDocument, its
reference count is set to 3 - by one more than its
decremented when it comes to be destroyed, so those
TXpDocuments 'hang around'.
- Ensure TXpDocument objects created by a call to
createDocument only have their reference count set to
'2' (not '3' as is currently the case), so that they
are correctly destroyed later
XpXmlDom.pas, createDocument (starting at line 462)
currently looks like this (apologies for lost formatting):
// -------------------------- //
function TXpDOMImplementation.createDocument(const
namespaceURI,
qualifiedName: DOMString; doctype: IDOMDocumentType):
IDOMDocument;
var
DocumentType : TXpDocumentType;
begin
if Assigned(doctype) then
DocumentType := GetXpNode(doctype) as TXpDocumentType
else
DocumentType := nil;
Result :=
TXpDOMDocument.Create(XpDOMImpl.CreateDocument(namespaceURI,
qualifiedName, DocumentType));
end;
// -------------------------- //
To 'fix' the momory leak, modify the method to this:
// -------------------------- //
function TXpDOMImplementation.createDocument(const
namespaceURI,
qualifiedName: DOMString; doctype: IDOMDocumentType):
IDOMDocument;
var
DocumentType: TXpDocumentType;
XpDocument: TXpDocument; // New
begin
if Assigned(doctype) then
DocumentType := GetXpNode(doctype) as TXpDocumentType
else
DocumentType := nil;
XpDocument := XpDOMImpl.CreateDocument(namespaceURI,
qualifiedName, DocumentType);
Result := TXpDOMDocument.Create(XpDocument);
XpDocument.Release; // THIS IS THE FIX!
end;
// -------------------------- //