Singletons Save Memory and Time

162 views
Skip to first unread message

Joe Barnhart

unread,
May 5, 2015, 7:13:27 PM5/5/15
to web...@googlegroups.com
I'm doing a lot of web2py programming lately, and much of my time is spent building helper classes that act as "factories" for high-powered web gadgets.  i have a class that builds jQuery DataTables, for example, and another that builds "widgets" as defined in my templating system (each of which is an elaborate construct of header, toolbar, body, etc.).

It occurred to me that many of these objects were by nature a "singleton" -- that is, a single instance of the object (the factory in this case) could be used over and over.  The alternative is to create a new instance and initialize it each time you use it -- and since it is used several times on each page, that means a lot of instances created and destroyed.

So how do you make something a singleton?  Step Zero is to make sure we are building these classes in a "module" instead of a model or a controller.  These must be compiled for their magic goodness to work properly.  Otherwise, there's no savings of effort, but the opposite!

Step one is to ensure there is nothing in the object's instance vars which is unique to that instance.  In other words, anything that changes from one use of the object to the next must be passed in from outside.  In my case the widgets are all driven from the database, so they don't hold any internal state to worry about.

Next, we need to determine how unique these instances are -- is there only one?  Is there potentially a different one on each distinct web page (controller/function)?  We use this to create a dictionary of instances that stores the object we create for each page in between invocations.

Last, we implement a "metaclass" that take care of the lookup of the singleton instance, or creates a new one if the singleton dictionary is empty.  The metaclass is the cleanest way of creating a Singleton.  There are other patterns in Python, but they all suffer from one problem or another.  Some replace the class object with a function -- those won't work if you subclass anything.  Of the three or four patterns I tried, this is the best.

Singleton class definition:

#!/usr/bin/env python
# coding: utf8

from gluon.globals import current

_instances = {}
class Singleton(type):
    def __call__(cls, *args, **kwargs):
        cr = current.request
        key = ('/').join([cr.controller,cr.function])
        if key not in _instances:
            _instances[key] = super(Singleton, cls).__call__(*args, **kwargs)
        return _instances[key]


This class keeps a dictionary of instances and uses a key of "contoller/function" from the current request to look up the Singleton.  Other dictionary keys are useful in other scenarios.  Also note that Singleton is a subclass of "type", not "object" as you may expect. 

To make a class (or even a superclass of classes) a Singleton, just declare the metaclass like so:


class WidgetHandler(object):
    if current.settings.singleton:
        __metaclass__ = Singleton
    
    def __init__(self, controller=None, title='', id=None, theme=None ):
        from gluon.storage import Storage
        from fcn import make_theme
        self.controller = controller
        self.params = Storage()
        self.id = id
        self.title = title
        self.theme = make_theme(theme)
        self.api = ['widget','content']
        self.scripts = []
        self.html_cache = None
        self.script_cache = None
        self.init_datatables()


The critical business is highlighted.  just declare this class to use "Singleton" as metaclass and it will magically become a Singleton.  I left the __init__ showing so you can see how elaborate initialization can be.  The call to "init_datatables" creates and initializes a jQuery Datatables object that, itself, requires a lot of steps.  It then builds the HTML representation of the widget, the javascripts that go with it, and so forth.  All of that effort is saved by bypassing this initialization each time.

In my case, WidgetHandler is itself used as a superclass by numerous WidgetXXX classes.  It holds the common template and reduces the amount of code in each widget class.  It also provides a single point of control for all of the common code.  Singleton "decorator" patterns do not work when creating a superclass because the class name is replaced with a function that generates a class, so super() fails to work properly.

Note the use of the global option "current.settings.singleton" to turn the Singleton feature off for debugging.  This will save some hair pulling as you work on the code, expecting it to use the code you just edited when the object has been cached instead!  It also provides an easy way to compare speed and memory usage with and without the Singleton feature.

Here is a sample of a widget happily sitting on its web page.  It is quick to load and refresh, because (a) it's compiled into a module and (b) it's a singleton!


Dave S

unread,
May 5, 2015, 7:31:33 PM5/5/15
to web...@googlegroups.com


On Tuesday, May 5, 2015 at 4:13:27 PM UTC-7, Joe Barnhart wrote:
I'm doing a lot of web2py programming lately, and much of my time is spent building helper classes that act as "factories" for high-powered web gadgets.  i have a class that builds jQuery DataTables, for example, and another that builds "widgets" as defined in my templating system (each of which is an elaborate construct of header, toolbar, body, etc.).
 
[...]
Here is a sample of a widget happily sitting on its web page.  It is quick to load and refresh, because (a) it's compiled into a module and (b) it's a singleton!




This is an impressive post.  I think I'm going to need to read it a couple of times.  Just to make sure I understand what I'm looking at,
the widget in the image has 3 sortable columns in the current tab, and a second tab as well?

/dps
 
 

Leonel Câmara

unread,
May 5, 2015, 8:42:41 PM5/5/15
to web...@googlegroups.com
Why not just use cache ram?

Joe Barnhart

unread,
May 5, 2015, 11:40:14 PM5/5/15
to web...@googlegroups.com
The jQuery Datatables library is very impressive.  It handles sorting, filtering, displaying of data in all forms.  Its really worth a look at http://datatables.net  My application is very data-centric and I use a LOT of tables, so I built some datatable factory methods to help me.

Joe Barnhart

unread,
May 5, 2015, 11:47:09 PM5/5/15
to web...@googlegroups.com
I don't want to cache the entire output of the function.  If I do that, the cache will include the data in the table.  I just want to save the object that generates the table and apply new data to it for each user / swimmer / whatever.  

Had I used cache.ram on this table, it would have made Natalie Coughlin's times a permanent feature of the cached output.  With the Singleton approach, I can click on Michael Phelps name and re-use the same object that generates the table, but with different table contents.

With cache.ram, I would eventually cache separate versions of the table for every swimmer I ever looked at.  Or at least within the given cache timeframe.

Michele Comitini

unread,
May 6, 2015, 7:04:33 AM5/6/15
to web...@googlegroups.com
if instantiating the object takes time Joe's approach is absolutely correct IMHO.  One must avoid  caching  per-thread or per-process data in the singleton unless explicitly managed with proper locking/message passing. 
So to speed up more one can use cache.ram or cache.disk or memcache or redis ... to also cache output, reducing also the impact on db and rendering as Leonel suggest, while delegating the complexity of managing context data to web2py caching.

--
Resources:
- http://web2py.com
- http://web2py.com/book (Documentation)
- http://github.com/web2py/web2py (Source code)
- https://code.google.com/p/web2py/issues/list (Report Issues)
---
You received this message because you are subscribed to the Google Groups "web2py-users" group.
To unsubscribe from this group and stop receiving emails from it, send an email to web2py+un...@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

Reply all
Reply to author
Forward
0 new messages