0

A few days back I had help from SO members in creating a safe plugins system, using interfaces to communicate between the main app and the dll's. This solved some problems I was having with Access Violations and memory leaks, and all is working perfectly now, without errors or crashes.

So, I've been creating some long due plugins for this project, which lead me to another problem: Speed

What I'm doing right now is, when the main app starts, loads all dll's in a specific folder that follow a determined name patern.

The code I'm using to load them is the following:

if FindFirst(cfg.ExePath+cPathPlugins+'\np*.npl', faAnyFile, SR)<>0
   then Exit; // npl files are in fact renamed dll's

PluginHost := TPluginHost.Create as IPluginHost;
Plugins    := TObjectList<TPluginInfo>.Create(True);

repeat
  if (SR.Attr <> faDirectory)
  then begin
    dll := LoadLibrary(PChar(cfg.ExePath+cPathPlugins+SR.Name));
    if dll<>0
    then begin
      @PluginInit := GetProcAddress(dll, 'PluginInitialize');
      if Assigned(PluginInit)
      then begin
        Plugin := TPluginInfo.Create;
        try
          Plugin.Dll  := dll;
          Plugin.Intf := PluginInit(PluginHost);
          Plugins.Add(Plugin);
        except
          Plugin.Free;
        end;
      end
      else FreeLibrary(dll);
    end;
  end;
until FindNext(SR)<>0;
System.SysUtils.FindClose(SR);

This bit of code takes about 45s to load 7 plugins. None of these dll's have initialization code, and the PluginInitialize just passes the host interface and retrieves the plugin interface.

My questions are:

  1. Can the number of methods on the interfaces affect the loading speed at this point? I don't believe so, but would like to confirm.

  2. Could this loading time be cut somehow, while still loading them at startup?

I did thought of an alternative, having the names of the plugins to load in the database and loading the dll itself only upon first usage, but I'd prefer not to apply this, as some of these plugins must run automatically upon completion of some events during the app's execution, and not only though a menu option on demand.

I thought maybe this could be done loading the plugins in background, on a different thread, but I don't know if this could bring any risks, since I've never used threads. I believe the main risk with using threads is when one tries to access variables that are being modified by the other, is this right? In this case, that wouldn't happen, I think, as what comes after the plugin loading is grabing the plugins name (using one of its methods) and adding it to a TButtonGroup, that is created before starting the search for the dll's.

Do you believe this would be a good solution? If not, any alternative you can point me to?

Ken White
  • 123,280
  • 14
  • 225
  • 444
nunopicado
  • 307
  • 4
  • 17
  • 1
    Same when not debugging? Anyway, you have to determine what takes time. [Profile](http://stackoverflow.com/questions/368938/delphi-profiling-tools). But with that timing, you can even manually profile. Just step over (F8) each line, observe which statement is noticeably slower. Then step into (F7) it if necessary. Etc.. – Sertac Akyuz Feb 21 '14 at 00:41
  • 1
    As @Sertac says, we can't debug this for you, because we don't have the DLLs you're loading or the entire code you're using to load them. Debug the code yourself, or find a profiler (Google "profiler Delphi") that can help you locate the problem. Your question is entirely too broad, anyway; the general rule here is "One specific question per post", and you've asked four (two that you've numbered, two more in your final paragraph) that are not specific at all. – Ken White Feb 21 '14 at 00:45
  • @nunopicado I am guessing that you might be compiling your "plugins" without using packages. How big are the DLL files? – Graymatter Feb 21 '14 at 00:49
  • Yes, I'm not using runtime packages. Dll's are about 12mb each. – nunopicado Feb 21 '14 at 00:53
  • 1
    Why would you need 12MB for a plugin? – David Heffernan Feb 21 '14 at 04:44
  • Weird. Loading a dll is pretty fast, since they are mapped in memory, and actually not fully loaded until needed. Sounds like an AntiVirus related issue. – Arnaud Bouchez Feb 21 '14 at 08:27
  • @DavidHeffernan: Indeed you are right, and your comment made me try to figure out why were all those files with just about the same size, even if they have radically different requirements, something I should have done before (shame on me). Turns out I was using a unit with several tools I created for the main app, and that came in handy when I started creating these plugins. Problem was, on that unit, I had a reference to the main app's datamodule, where I have the reportbuilder components. All but one was largelly reduced, and that one was expected as it is indeed quite large.Much faster now – nunopicado Feb 21 '14 at 23:31
  • @ArnaudBouchez: The computer has indeed an AntiVirus solution. But turns out that wasn't the issue. It was really a matter of bad programming. – nunopicado Feb 21 '14 at 23:37
  • These 7 dll's now range from 2.6MB to 10.5MB, making a total of 45mb instead of 84MB. With only four of them revised, the speed of the same bit of code had been upgraded to around 10 seconds (even faster than the size reduced could suggest). I guess in fairness, @DavidHeffernan's comment was the best answer to the problem I had. – nunopicado Feb 21 '14 at 23:50

1 Answers1

3

Your problem is that the DLL's are large. You need to create the DLL's using run time packages. That way, the code is only loaded once. Each DLL will include duplicates of the same code. LoadLibrary will load the DLL and call the initialization code for each DLL. This means that package X would be linked into each plugin that uses it and would be initialized when each plug in is loaded. (corrected)

For standalone EXE file, taking off runtime packages is great. It makes deployment much simpler. When you want to start using a plugin system, it's best to switch to a system that includes runtime packages.

That doesn't mean that you need to keep every runtime package separate. For example, if you only use Dev Express controls in the main application or in a single plugin then you can let Delphi compile that package into the App/DLL.

To change which runtime packages you wish to keep separate and which ones you wish to include in the project go to the "Packages-Runtime Packages" page in the project options. There is a check box that lets you choose to link with runtime packages. Underneath is a text box. In this text box you can specify the names of the packages that you want to keep separate.

Graymatter
  • 6,529
  • 2
  • 30
  • 50
  • 1
    Or spill your common code from all plugins over to a resource DLL that they all can share. Load this DLL from the host process, and pass its handle into each plugin DLL to use – Jerry Dodge Feb 21 '14 at 01:00
  • 2
    Yes, that helps too. Looking at the 12 meg size I have a sneaky feeling that it's something like the DevExpress controls that are adding a lot. – Graymatter Feb 21 '14 at 01:02
  • 1
    Yeah, any kind of library like that can cause bloated file size. Which means you have to scope out your `uses` clauses everywhere and make sure you're not using anything you don't need – Jerry Dodge Feb 21 '14 at 01:03
  • So if in those dll's I only use, for instance, VCL forms, Zeos Lib and Report Builder, I'd only need to deploy those bpl, and not all of them? I'll have to look into it... I've been avoiding having to send bpl's along, for easier maintenance of the instalations. So, I'd put in that box the packages I need to send, or the ones I don't want to send? – nunopicado Feb 21 '14 at 01:05
  • 1
    Huge? 12MB code is not run when you load the library, it's just mapped into the address space. I don't think the first paragraph makes any sense. Granted the 'PluginInit' might be taking time, but it's to be determined, and may not change a lot when using packages. – Sertac Akyuz Feb 21 '14 at 01:10
  • 1
    @nunopicado You put the names of the packages that you would like to make external. Some of those packages can include multiple runtime BPL's and you don't always need to include all of them. – Graymatter Feb 21 '14 at 01:11
  • @Graymatter: Actually, I'm not using devexpress in this project. On some of them I'm using ReportBuilder, but curious thing, all plugins are more or less the same size, and only three use ReportBuilder. One uses XML data binding, another calls a function in another dll to communicate SOAP requests, and another one is a simple calculator, pure VCL. What all of them have in common, though, is the use of Vcl.Styles. – nunopicado Feb 21 '14 at 01:11
  • 1
    @nunopicado ReportBuilder is also quite big so it's more than likely a combination of that and the VCL runtime. – Graymatter Feb 21 '14 at 01:12
  • 2
    @SertacAkyuz When you load a DLL the initialization code for all of the units included in the DLL are executed. The LoadLibrary will also load the full 12 Meg into RAM for each one. I updated the first paragraph to make this clearer. – Graymatter Feb 21 '14 at 01:15
  • @SertacAkyuz: I added a date/time based log (simply registering the time in a stringlist after each statement), and PluginInit is actually very fast, just a few miliseconds. The statement that took longer was the LoadLibrary. – nunopicado Feb 21 '14 at 01:17
  • 1
    nuno - It should be taking about 6 seconds. @Gray is right about unit initialization code, but it is too long a time IMO to doubt that it's just the normal time for initialization of some larger library. – Sertac Akyuz Feb 21 '14 at 01:22
  • 1
    @SertacAkyuz It's not just the initialization but the loading as well. 7 DLL's = 85 Meg. 45 seconds does sound like a long time but it depends a lot on the speed of the hard drive and what initialization code is being executed in those libraries. I have seen some 3rd party vendors do some nasty stuff in their initialization. In one case a developer was even checking for updates on the internet. – Graymatter Feb 21 '14 at 01:33
  • @Gray - *"I have seen .."* - That would be the kind of problem I expect to be found here. If that turns out to be the case, it is to be corrected. Employing packages won't help. – Sertac Akyuz Feb 21 '14 at 01:38
  • @SertacAkyuz - Actually it will as the initialization code won't be executed again for each DLL but will only be executed once for the host application. Even if that is the case, it still makes sense to use packages for any plugin system. It gives a lot of flexibility and also has the added benefit of shrinking the size of the deployment considerably. – Graymatter Feb 21 '14 at 01:42
  • @Gray - But why do you think that you have to load the dll more than once? – Sertac Akyuz Feb 21 '14 at 01:44
  • 1
    @SertacAkyuz It's not the same DLL that is loaded more than once, it's the "package". Lets say that package X is has a bit of nasty code in it. We include package X in 7 plug in DLL's and in the host application and we don't use runtime packages. Every time we load a plug in DLL (and the host app), the initialization code for X is executed. That means this bad code will be executed 8 times. Once for the host app and once for each DLL that uses it. Even if there isn't bad code, that still a lot of unnecessary execution of code and a waste of memory. – Graymatter Feb 21 '14 at 01:46
  • @nunopicado Just a question of curiosity... Are these plugins always going to be developed by you in Delphi, or are you making it open so third-parties can create plugins? – Jerry Dodge Feb 21 '14 at 01:53
  • 1
    @Gray - OK, so if styles turns out to be the culprit for instance, then it will be run only once instead of 8 times, is it? BTW, I'm pretty confident that libraries are loaded through memory mapped files, so loading to ram would not be an issue. – Sertac Akyuz Feb 21 '14 at 01:58
  • @JerryDodge: For the foreseeable future, only I will be creating them. It's a Point of Sale software, and these plugins are only to add functionality without having to touch the main app. These plugins have, in theory, 5 possible starters: On Demand, On Boot, Daily, Monthly and Annually. Of course not all of them use all of these events to start. The plugin itself will determine that, by virtualizing the interface methods that it doesn't use (for example, if a plugin is not meant to run on boot, that method will be made virtual abstract). – nunopicado Feb 21 '14 at 02:02
  • Then it sounds like we're in the same boat :-) I too work on a point-of-sale software, but it's a legacy system that's existed since Delphi has existed. And it's rather large with discombobulated code from over 200 developers around the world. What I would give to be a part of the re-write, which I've already attempted a few times but got nowhere. – Jerry Dodge Feb 21 '14 at 02:06
  • @JerryDodge: :) Mine is a recent system. Development started 28 Dec 2012, but only recently (September) I created the plugin system - and a bad one, I might add, as it was giving me AV's in any computer other than my own. When I switched to this interface system, all problems were gone - all but speed of course. The computer where this problem is most noticed is not exactly top notch, so it has something to do with it as well. Something curious though, if I exit the software and start it again, it takes about half the time to load. Probably reading something from ram. – nunopicado Feb 21 '14 at 02:12
  • 1
    @Graymatter: Citation for "LoadLibrary will load the whole DLL into memory", please. AFAIK, this is not true; the entire DLL is *not* loaded into memory. It's loaded via a memory-mapped file, and only the exports table is actually loaded into memory. I find it very hard to believe that every single application loads the entire contents of every DLL it uses into the process address space simultaneously; simply loading all of the system DLLs that are referenced would eat the entire address space of a Win32 app. – Ken White Feb 21 '14 at 02:14
  • @KenWhite Agreed 100% – Jerry Dodge Feb 21 '14 at 02:16
  • Did you rebase the DLLS so that they don't overlap with other modules in the process and each other? In principle a DLL load can be not much more the a call to CreateFileMapping – David Heffernan Feb 21 '14 at 04:32
  • @KenWhite You are correct. I am still remembering from a long time back when all DLL's were loaded completely into memory. I did some research and this changed with the NT kernel. Sometimes one never gives these deep seated thoughts a second glance. – Graymatter Feb 21 '14 at 04:39
  • 1
    Packages kind of suck for plugins. They compel the author to use the exact same dev tool as you. That's a horrible restriction to impose. – David Heffernan Feb 21 '14 at 04:45
  • @DavidHeffernan Yes and no. If you are sharing common code across multiple plugins it makes sense to use packages. Not using packages makes things more complicated for the average developer. My usual approach when I need to support other development tools is to build a plugin specifically for other tools at the time that I need to support them. Sort of a proxy plug in. – Graymatter Feb 21 '14 at 04:52
  • Ok. Proxy plug-in is just fine. Gets you all the benefits without imposing constraints on other developers. – David Heffernan Feb 21 '14 at 05:03
  • 1
    @DavidHeffernan One more thing, there are two different concepts of packages that are in play. The first is sharing BPL's between different DLL's, the second is using the package system for your plugins. I normally use both but there is no reason why the first can't be used without the second. In this case, the OP is building DLL's and is linking, as an example, all of the ReportBuilder code into each DLL. – Graymatter Feb 21 '14 at 05:13
  • @Ken, every used DLL does take up address space, even if it's not actually copied into main memory. Reserving address space is what file mapping *does*. If you don't have enough address space to hold all your DLLs, then loading will ultimately fail. – Rob Kennedy Feb 21 '14 at 05:25
  • 1
    @Rob: Yes, some address space. Not the equivalent of the DLL's size, however. A call to LoadLibrary for a 12 MB DLL does not require 12 MB of actual address space. – Ken White Feb 21 '14 at 05:27
  • @Ken Yes it does. 12MB of contiguous address space. – David Heffernan Feb 21 '14 at 23:41
  • @David: As I asked Graymatter, a citation is needed that clearly states that a simple call to LoadLibrary immediately loads all of the DLLs code and data into actual physical memory, which is the topic of this discussion. (We were discussing the performance impact of making calls to LoadLibrary for the plugins here.) Note that the comment you responded to said "actual", not "virtual". It may allocate 12MB of address space, but it doesn't actually *use* that address space to load the DLL content until it's needed. – Ken White Feb 22 '14 at 00:11
  • @Ken I don't think any citations are needed. The loader is pretty well understood. Rob's comment is accurate. – David Heffernan Feb 22 '14 at 06:26
  • @David: OK. I disagree. No offense intended, but because you say so doesn't make it fact. :-) – Ken White Feb 22 '14 at 06:28
  • @Ken You disagree that a citation is needed? Or that a module can be loaded without reserving address space. – David Heffernan Feb 22 '14 at 06:29
  • @David: I disagree with "no citation is needed", because I think your statement is incorrect. Because your comment said "Yes, it does" doesn't make the information factual, and I asked for a citation to indicate it was correct. Please provide a specific citation that says that a simple call to LoadLibrary immediately loads the entire DLL code and data segments into actual memory (rather than just reserving address space). Reserving 12MB of address space is pretty much instantaneous, while actually loading the DLL into RAM would take many cycles. The first happens, the latter doesn't. – Ken White Feb 22 '14 at 06:40
  • @KenWhite I did not state that, and it's not true. LoadLibrary maps the memory, that is reserves the address space. It is then faulted in if needed. I think it has to be loaded and committed if the base address does not match so that fixups can be performed. You asserted that Rob's comment was wrong. It is not. – David Heffernan Feb 22 '14 at 06:44
  • @David: No, I didn't. I said "A call to LoadLibrary for a 12 MB DLL does not require 12 MB of **actual** address space**. Actual address space is not virtual address space - actual address space is what is currently in RAM. – Ken White Feb 22 '14 at 06:50
  • @Ken No. Your terminology is wrong. You mean allocated or committed memory. As opposed to reserved address space. – David Heffernan Feb 22 '14 at 06:58
  • @David: My [original comment](http://stackoverflow.com/questions/21922709/delphi-speed-up-loading-of-interfaced-plugins-dll/21923027?noredirect=1#comment33207785_21923027) was "the entire DLL is not loaded into memory", which is what Rob commented on. My statement was correct - the entire DLL is not **loaded into memory**. It may have address space allocated. – Ken White Feb 22 '14 at 07:14