1 {
2  *****************************************************************************
3   This file is part of the Lazarus Component Library (LCL)
4 
5   See the file COPYING.modifiedLGPL.txt, included in this distribution,
6   for details about the license.
7  *****************************************************************************
8 
9   Author: Mattias Gaertner
10 
11   Abstract:
12     Types and methods to cache interface resources.
13     See graphics.pp for examples.
14 }
15 unit LCLResCache;
16 
17 {$mode objfpc}{$H+}
18 
19 interface
20 
21 uses
22   Classes, SysUtils, Types, Laz_AVL_Tree,
23   // LazUtils
24   FPCAdds, LazLoggerBase, LazTracer,
25   // LCL
26   LCLType, WSReferences,
27   syncobjs; // This FCL unit must be in the end.
28 
29 {off $DEFINE CheckResCacheConsistency}
30 
31 type
32   TResourceCache = class;
33   TResourceCacheDescriptor = class;
34 
35   { TResourceCacheItem }
36 
37   TResourceCacheItem = class
38   protected
39     FDestroying: boolean;
40     FReferenceCount: integer;
41   public
42     Handle: TLCLHandle;
43     Cache: TResourceCache;
44     FirstDescriptor, LastDescriptor: TResourceCacheDescriptor;
45     Next, Prev: TResourceCacheItem;
46     constructor Create(TheCache: TResourceCache; TheHandle: TLCLHandle);
47     destructor Destroy; override;
48     procedure IncreaseRefCount;
49     procedure DecreaseRefCount;
50     procedure AddToList(var First, Last: TResourceCacheItem);
51     procedure RemoveFromList(var First, Last: TResourceCacheItem);
52     procedure WarnReferenceHigh; virtual;
53   public
54     property ReferenceCount: integer read FReferenceCount;
55   end;
56   TResourceCacheItemClass = class of TResourceCacheItem;
57 
58 
59   { TResourceCacheDescriptor }
60 
61   TResourceCacheDescriptor = class
62   protected
63     FDestroying: boolean;
64   public
65     Item: TResourceCacheItem;
66     Cache: TResourceCache;
67     Next, Prev: TResourceCacheDescriptor;
68     constructor Create(TheCache: TResourceCache; TheItem: TResourceCacheItem);
69     destructor Destroy; override;
70     procedure AddToList(var First, Last: TResourceCacheDescriptor);
71     procedure RemoveFromList(var First, Last: TResourceCacheDescriptor);
72   end;
73   TResourceCacheDescriptorClass = class of TResourceCacheDescriptor;
74 
75 
76   { TResourceCache }
77 
78   TResourceCache = class
79   protected
80     FItems: TAvlTree;
81     FDescriptors: TAvlTree;
82     FDestroying: boolean;
83     FResourceCacheDescriptorClass: TResourceCacheDescriptorClass;
84     FResourceCacheItemClass: TResourceCacheItemClass;
85     FMaxUnusedItem: integer; // how many freed resources to keep
86     FFirstUnusedItem, FLastUnusedItem: TResourceCacheItem;
87     FUnUsedItemCount: integer;
88     FLock: TCriticalSection;
89     procedure RemoveItem(Item: TResourceCacheItem); virtual;
90     procedure RemoveDescriptor(Desc: TResourceCacheDescriptor); virtual;
91     procedure ItemUsed(Item: TResourceCacheItem);
92     procedure ItemUnused(Item: TResourceCacheItem);
ItemIsUsednull93     function ItemIsUsed(Item: TResourceCacheItem): boolean;
94   public
95     constructor Create;
96     procedure Clear;
97     destructor Destroy; override;
CompareItemsnull98     function CompareItems(Tree: TAvlTree; Item1, Item2: Pointer): integer; virtual;
CompareDescriptorsnull99     function CompareDescriptors(Tree: TAvlTree; Desc1, Desc2: Pointer): integer; virtual; abstract;
100     procedure ConsistencyCheck;
101     procedure Lock;
102     procedure Unlock;
103   public
104     property MaxUnusedItem: integer read FMaxUnusedItem
105                                            write FMaxUnusedItem;
106     property ResourceCacheItemClass: TResourceCacheItemClass
107                                                    read FResourceCacheItemClass;
108     property ResourceCacheDescriptorClass: TResourceCacheDescriptorClass
109                                              read FResourceCacheDescriptorClass;
110   end;
111 
112 
113   { THandleResourceCache }
114 
115   THandleResourceCache = class(TResourceCache)
116   public
FindItemnull117     function FindItem(Handle: TLCLHandle): TResourceCacheItem;
118   end;
119 
120 
121   { TBlockResourceCacheDescriptor }
122 
123   TBlockResourceCacheDescriptor = class(TResourceCacheDescriptor)
124   public
125     Data: Pointer;
126     destructor Destroy; override;
127   end;
128 
129 
130   { TBlockResourceCache }
131 
132   TBlockResourceCache = class(THandleResourceCache)
133   private
134     FDataSize: integer;
135   protected
136     FOnCompareDescPtrWithDescriptor: TListSortCompare;
137   public
138     constructor Create(TheDataSize: integer);
FindDescriptornull139     function FindDescriptor(DescPtr: Pointer): TBlockResourceCacheDescriptor;
AddResourcenull140     function AddResource(Handle: TLCLHandle; DescPtr: Pointer): TBlockResourceCacheDescriptor;
CompareDescriptorsnull141     function CompareDescriptors(Tree: TAvlTree; Desc1, Desc2: Pointer): integer; override;
142   public
143     property DataSize: integer read FDataSize;
144     property OnCompareDescPtrWithDescriptor: TListSortCompare
145                                            read FOnCompareDescPtrWithDescriptor;
146   end;
147 
ComparePHandleWithResourceCacheItemnull148 function ComparePHandleWithResourceCacheItem(HandlePtr: PLCLHandle;
149   Item: TResourceCacheItem): integer;
CompareDescPtrWithBlockResDescnull150 function CompareDescPtrWithBlockResDesc(DescPtr: Pointer;
151   Item: TBlockResourceCacheDescriptor): integer;
152 
153 implementation
154 
155 
CompareLCLHandlesnull156 function CompareLCLHandles(h1, h2: TLCLHandle): integer;
157 begin
158   if h1>h2 then
159     Result:=1
160   else if h1<h2 then
161     Result:=-1
162   else
163     Result:=0;
164 end;
165 
ComparePHandleWithResourceCacheItemnull166 function ComparePHandleWithResourceCacheItem(HandlePtr: PLCLHandle;
167   Item: TResourceCacheItem): integer;
168 begin
169   Result := CompareLCLHandles(HandlePtr^, Item.Handle);
170 end;
171 
CompareDescPtrWithBlockResDescnull172 function CompareDescPtrWithBlockResDesc(DescPtr: Pointer;
173   Item: TBlockResourceCacheDescriptor): integer;
174 begin
175   Result := CompareMemRange(DescPtr, Item.Data,
176               TBlockResourceCache(Item.Cache).DataSize);
177 end;
178 
179 
180 { TResourceCacheItem }
181 
182 constructor TResourceCacheItem.Create(TheCache: TResourceCache; TheHandle: TLCLHandle);
183 begin
184   Cache := TheCache;
185   Handle := TheHandle;
186 end;
187 
188 destructor TResourceCacheItem.Destroy;
189 begin
190   if FDestroying then
191     RaiseGDBException('');
192   FDestroying := True;
193   Cache.RemoveItem(Self);
194   //debugln('TResourceCacheItem.Destroy B ',dbgs(Self));
195   Handle := 0;
196   inherited Destroy;
197   //debugln('TResourceCacheItem.Destroy END ',dbgs(Self));
198 end;
199 
200 procedure TResourceCacheItem.IncreaseRefCount;
201 begin
202   inc(FReferenceCount);
203   if FReferenceCount = 1 then
204     Cache.ItemUsed(Self);
205   {$IFDEF VerboseResCache}
206   if FReferenceCount = 10 then
207     begin
208     WarnReferenceHigh;
209     DumpStack;
210     end;
211   {$ENDIF}
212   if (FReferenceCount = 1000) or (FReferenceCount = 10000) then
213     WarnReferenceHigh;
214 end;
215 
216 procedure TResourceCacheItem.DecreaseRefCount;
217 
218   procedure RaiseRefCountZero;
219   begin
220     RaiseGDBException('TResourceCacheItem.DecreaseRefCount=0 '+ClassName);
221   end;
222 
223 begin
224   //debugln('TResourceCacheItem.DecreaseRefCount ',ClassName,' ',dbgs(Self),' ',dbgs(FReferenceCount));
225   if FReferenceCount = 0 then
226     RaiseRefCountZero;
227   dec(FReferenceCount);
228   if FReferenceCount = 0  then
229     Cache.ItemUnused(Self);
230   //debugln('TResourceCacheItem.DecreaseRefCount END ');
231 end;
232 
233 procedure TResourceCacheItem.AddToList(var First, Last: TResourceCacheItem);
234 // add as last
235 begin
236   Next := nil;
237   Prev := Last;
238   Last := Self;
239   if First = nil then First := Self;
240   if Prev <> nil then Prev.Next := Self;
241 end;
242 
243 procedure TResourceCacheItem.RemoveFromList(var First,Last: TResourceCacheItem);
244 begin
245   if First = Self then First := Next;
246   if Last = Self then Last := Prev;
247   if Next <> nil then Next.Prev := Prev;
248   if Prev <> nil then Prev.Next := Next;
249   Next := nil;
250   Prev := nil;
251 end;
252 
253 procedure TResourceCacheItem.WarnReferenceHigh;
254 begin
255   {$IFNDEF DisableChecks}
256   debugln('WARNING: TResourceCacheItem.IncreaseRefCount ',dbgs(FReferenceCount),' ',Cache.ClassName);
257   {$ENDIF}
258 end;
259 
260 { TResourceCacheDescriptor }
261 
262 constructor TResourceCacheDescriptor.Create(TheCache: TResourceCache;
263   TheItem: TResourceCacheItem);
264 begin
265   Cache := TheCache;
266   Item := TheItem;
267   Item.IncreaseRefCount;
268   AddToList(Item.FirstDescriptor, Item.LastDescriptor);
269 end;
270 
271 destructor TResourceCacheDescriptor.Destroy;
272 begin
273   if FDestroying then
274     RaiseGDBException('');
275   FDestroying := True;
276   Cache.RemoveDescriptor(Self);
277   inherited Destroy;
278 end;
279 
280 procedure TResourceCacheDescriptor.AddToList(var First, Last: TResourceCacheDescriptor);
281 // add as last
282 begin
283   Next := nil;
284   Prev := Last;
285   Last := Self;
286   if First = nil then First := Self;
287   if Prev <> nil then Prev.Next := Self;
288 end;
289 
290 procedure TResourceCacheDescriptor.RemoveFromList(var First, Last: TResourceCacheDescriptor);
291 begin
292   if First = Self then First := Next;
293   if Last = Self then Last := Prev;
294   if Next <> nil then Next.Prev := Prev;
295   if Prev <> nil then Prev.Next := Next;
296   Next := nil;
297   Prev := nil;
298 end;
299 
300 { TResourceCache }
301 
302 procedure TResourceCache.RemoveItem(Item: TResourceCacheItem);
303 begin
304   if not FDestroying then
305   begin
306     while Item.FirstDescriptor <> nil do
307     begin
308       if Item.FirstDescriptor.FDestroying then
309         RaiseGDBException('TResourceCache.RemoveItem');
310       Item.FirstDescriptor.Free;
311     end;
312     FItems.Remove(Item);
313   end;
314 end;
315 
316 procedure TResourceCache.RemoveDescriptor(Desc: TResourceCacheDescriptor);
317 var
318   Item: TResourceCacheItem;
319 begin
320   if not FDestroying then
321   begin
322     Item := Desc.Item;
323     if Item <> nil then
324       Desc.RemoveFromList(Item.FirstDescriptor, Item.LastDescriptor);
325     FDescriptors.Remove(Desc);
326     if (Item <> nil) and (Item.FirstDescriptor = nil) and (not Item.FDestroying) then
327       Item.Free;
328   end;
329 end;
330 
331 procedure TResourceCache.ItemUsed(Item: TResourceCacheItem);
332 // called after creation or when Item is used again
333 begin
334   if not ItemIsUsed(Item) then
335   begin
336     // remove from unused list
337     Item.RemoveFromList(FFirstUnusedItem, FLastUnusedItem);
338     dec(FUnUsedItemCount);
339   end;
340 end;
341 
342 procedure TResourceCache.ItemUnused(Item: TResourceCacheItem);
343 // called when Item is not used any more
344 var
345   DeleteItem: TResourceCacheItem;
346 begin
347   {$IFDEF CheckResCacheConsistency}
348   ConsistencyCheck;
349   {$ENDIF}
350   //debugln('TResourceCache.ItemUnused A ',ClassName,' ',dbgs(Self));
351   if not ItemIsUsed(Item) then
352     raise Exception.Create('TResourceCache.ItemUnused');
353   //debugln('TResourceCache.ItemUnused B ',ClassName,' ',dbgs(Self));
354   Item.AddToList(FFirstUnusedItem, FLastUnusedItem);
355   inc(FUnUsedItemCount);
356   //debugln('TResourceCache.ItemUnused C ',ClassName,' ',dbgs(Self));
357   if FUnUsedItemCount > FMaxUnusedItem then
358   begin
359     // maximum unused resources reached -> free the oldest
360     DeleteItem := FFirstUnusedItem;
361     DeleteItem.RemoveFromList(FFirstUnusedItem, FLastUnusedItem);
362     DeleteItem.Free;
363   end;
364   //debugln('TResourceCache.ItemUnused END ',ClassName,' ',dbgs(Self));
365 end;
366 
ItemIsUsednull367 function TResourceCache.ItemIsUsed(Item: TResourceCacheItem): boolean;
368 begin
369   Result := (FFirstUnusedItem <> Item) and (Item.Next = nil) and (Item.Prev = nil)
370 end;
371 
372 constructor TResourceCache.Create;
373 begin
374   FMaxUnusedItem := 100;
375   FItems := TAvlTree.CreateObjectCompare(@CompareItems);
376   FDescriptors := TAvlTree.CreateObjectCompare(@CompareDescriptors);
377   FResourceCacheItemClass := TResourceCacheItem;
378   FResourceCacheDescriptorClass := TResourceCacheDescriptor;
379   FLock := TCriticalSection.Create;
380 end;
381 
382 procedure TResourceCache.Clear;
383 begin
384   while FFirstUnusedItem <> nil do
385     FFirstUnusedItem.RemoveFromList(FFirstUnusedItem, FLastUnusedItem);
386   FItems.FreeAndClear;
387   FDescriptors.FreeAndClear;
388 end;
389 
390 destructor TResourceCache.Destroy;
391 begin
392   FDestroying := True;
393   Clear;
394   FItems.Free;
395   FItems := nil;
396   FDescriptors.Free;
397   FDescriptors := nil;
398   FLock.Free;
399   inherited Destroy;
400 end;
401 
CompareItemsnull402 function TResourceCache.CompareItems(Tree: TAvlTree; Item1, Item2: Pointer): integer;
403 begin
404   Result := CompareLCLHandles(TResourceCacheItem(Item1).Handle,
405                               TResourceCacheItem(Item2).Handle);
406 end;
407 
408 procedure TResourceCache.ConsistencyCheck;
409 var
410   ANode: TAvlTreeNode;
411   Item: TResourceCacheItem;
412   Desc: TResourceCacheDescriptor;
413   Desc2: TResourceCacheDescriptor;
414 begin
415   if (FFirstUnusedItem=nil) xor (FLastUnusedItem=nil) then
416     RaiseGDBException('');
417 
418   // check items
419   FItems.ConsistencyCheck;
420   ANode := FItems.FindLowest;
421   while ANode <> nil do
422   begin
423     Item := TResourceCacheItem(ANode.Data);
424     if Item.FirstDescriptor = nil then
425       RaiseGDBException('');
426     if Item.LastDescriptor = nil then
427       RaiseGDBException('');
428     if Item.FirstDescriptor.Prev <> nil then
429       RaiseGDBException('');
430     if Item.LastDescriptor.Next <> nil then
431       RaiseGDBException('');
432     Desc := Item.FirstDescriptor;
433     while Desc <> nil do
434     begin
435       if Desc.Item <> Item then
436         RaiseGDBException('');
437       if (Desc.Next <> nil) and (Desc.Next.Prev <> Desc) then
438         RaiseGDBException('');
439       if (Desc.Prev <> nil) and (Desc.Prev.Next <> Desc) then
440         RaiseGDBException('');
441       if (Desc.Next = nil) and (Item.LastDescriptor <> Desc) then
442         RaiseGDBException('');
443       Desc := Desc.Next;
444     end;
445     ANode := FItems.FindSuccessor(ANode);
446   end;
447 
448   // check Descriptors
449   FDescriptors.ConsistencyCheck;
450   ANode := FDescriptors.FindLowest;
451   while ANode <> nil do
452   begin
453     Desc := TResourceCacheDescriptor(ANode.Data);
454     Item := Desc.Item;
455     if Item = nil then
456       RaiseGDBException('');
457     Desc2 := Item.FirstDescriptor;
458     while (Desc2 <> nil) and (Desc2 <> Desc) do
459       Desc2 := Desc2.Next;
460     if Desc <> Desc2 then
461       RaiseGDBException('');
462     ANode := FItems.FindSuccessor(ANode);
463   end;
464 end;
465 
466 procedure TResourceCache.Lock;
467 begin
468   FLock.Enter;
469 end;
470 
471 procedure TResourceCache.Unlock;
472 begin
473   FLock.Leave;
474 end;
475 
476 { THandleResourceCache }
477 
FindItemnull478 function THandleResourceCache.FindItem(Handle: TLCLHandle): TResourceCacheItem;
479 var
480   ANode: TAvlTreeNode;
481 begin
482   ANode := FItems.FindKey(@Handle,
483                           TListSortCompare(@ComparePHandleWithResourceCacheItem));
484   if ANode <> nil then
485     Result := TResourceCacheItem(ANode.Data)
486   else
487     Result := nil;
488 end;
489 
490 { TBlockResourceCache }
491 
492 constructor TBlockResourceCache.Create(TheDataSize: integer);
493 begin
494   inherited Create;
495   FDataSize := TheDataSize;
496   FResourceCacheDescriptorClass := TBlockResourceCacheDescriptor;
497   FOnCompareDescPtrWithDescriptor := TListSortCompare(@CompareDescPtrWithBlockResDesc);
498 end;
499 
TBlockResourceCache.FindDescriptornull500 function TBlockResourceCache.FindDescriptor(DescPtr: Pointer): TBlockResourceCacheDescriptor;
501 var
502   ANode: TAvlTreeNode;
503 begin
504   ANode := FDescriptors.FindKey(DescPtr,FOnCompareDescPtrWithDescriptor);
505   if ANode <> nil then
506     Result := TBlockResourceCacheDescriptor(ANode.Data)
507   else
508     Result := nil;
509 end;
510 
TBlockResourceCache.AddResourcenull511 function TBlockResourceCache.AddResource(Handle: TLCLHandle; DescPtr: Pointer): TBlockResourceCacheDescriptor;
512 var
513   Item: TResourceCacheItem;
514 
515   procedure RaiseDescriptorAlreadyAdded;
516   var
517     Msg: String;
518     i: Integer;
519   begin
520     Msg:='TBlockResourceCache.AddResource Descriptor Already Added '+LineEnding;
521     for i:=0 to DataSize-1 do
522       Msg:=Msg+HexStr(ord(PChar(DescPtr)[i]),2);
523     raise Exception.Create(Msg);
524   end;
525 
526 begin
527   {$IFDEF CheckResCacheConsistency}
528   ConsistencyCheck;
529   {$ENDIF}
530   Result := FindDescriptor(DescPtr);
531   if Result <> nil then
532     RaiseDescriptorAlreadyAdded;
533 
534   Item := FindItem(Handle);
535   if Item = nil then
536   begin
537     Item := FResourceCacheItemClass.Create(Self, Handle);
538     FItems.Add(Item);
539   end;
540   Result := TBlockResourceCacheDescriptor(FResourceCacheDescriptorClass.Create(Self, Item));
541   ReAllocMem(Result.Data, DataSize);
542   System.Move(DescPtr^, Result.Data^, DataSize);
543   FDescriptors.Add(Result);
544 end;
545 
TBlockResourceCache.CompareDescriptorsnull546 function TBlockResourceCache.CompareDescriptors(Tree: TAvlTree; Desc1, Desc2: Pointer): integer;
547 begin
548   Result := CompareMemRange(TBlockResourceCacheDescriptor(Desc1).Data,
549                             TBlockResourceCacheDescriptor(Desc2).Data,
550                             DataSize);
551 end;
552 
553 { TBlockResourceCacheDescriptor }
554 
555 destructor TBlockResourceCacheDescriptor.Destroy;
556 begin
557   inherited Destroy;
558   ReAllocMem(Data, 0);
559 end;
560 
561 end.
562