Phantasmal MUD Lib for DGD

Phantasmal Site > DGD > Running a MUD > Scalability

How Far Will DGD Scale?

From: dgd at list.imaginary.com (Erwin Harte)
Date: Mon Nov 12 11:39:01 2001
Subject: [DGD] Users

On Mon, Nov 12, 2001 at 06:18:13PM +0100, Troels Therkelsen wrote:
[...]
> 
> Someone more into the black magic of the internals of the DGD driver will
> have to answer what the max value you can safely set users to is.

DGD will let you go up to whatever EINDEX_MAX is:

  ---- Quote from dgd/src/config.c ----
                                { "users",              INT_CONST, FALSE,
                                                        1, EINDEX_MAX },
  ----

In config.h you'll see that EINDEX_MAX is UCHAR_MAX, which I would
assume to be 255?

Of course if you change the typedef for eindex to something bigger and
change the EINDEX_MAX and EINDEX defines then you can go further, as
long as your operating system or user-limits don't stop you sooner.

Regards,

Erwin.
-- 
Erwin Harte

From: dgd at list.imaginary.com (Erwin Harte)
Date: Wed Dec 24 11:21:01 2003
Subject: [DGD] 4 bytes should be enough for anyone.

Nice gotcha I ran into this morning, thought it would be worth sharing
it and its solution with others:

I use a heuristic to occasionally force a swapout() when a lot of
memory has been allocated but a significant amount is not in use
(anymore), to avoid cluttering up the memory from the operating
system's point of view.

The condition I used is (simplified):

    if (mem_used < 2 * mem_allocated / 3) {
        /* trigger swapout */
    }

So if I have 120MB allocated and less than 80MB is in use, it'll do
that quite nicely.

However, this morning the memory usage rose to about 1.5GB (1513MB to
be precise) and strangely enough the swapout didn't trigger.  Closer
inspection revealed that:

    1513 * 1024 * 1024 = 1586495488

    1586495488 * 2 = -1121976320

    1586495488 * 2 / 3 = -373992106

Uhm, oops, we ran into the limit of integers and since memory usage is
never going to be negative, it would happily continue to drag those
1.5GB of virtual memory around.

This should work without running into this particular problem:

    if (mem_used / 2 < mem_allocated / 3) {
        /* trigger swapout */
    }

:)

Cheers,

Erwin.
-- 
Erwin Harte

From: dgd at list.imaginary.com (Par Winzell)
Date: Sat Apr  3 15:36:07 2004
Subject: [DGD] Re: Clones and very large arrays

Robert,

> So is there anything to stop me from making it an unsigned long, giving 
> me a generous 2.1 million elements or so? I know 2.1 million isn't 
> feasable for obvious reasons, but this is just a cap, right?

The basic reason (as I understand it) there is a limit is because DGD
implements powerful operators in the core language, such as array
addition and string addition, whose execution times are more or less
linearly proportional to the size of the operands (arrays).

Whenever linear operations are used for fundamental operations (clones
of a clonable is a perfect example), scalability goes out the window. 
Making a game truly scale is surprisingly difficult, and pretty much the 
first thing you do is get rid of every single place where you use an 
array to store anything substantial.

If there are N clones of an object and cloning a new object takes time 
proportional to N, then it takes N^2 time to clone all those objects. 
That's the crux of the problem. It means that if your prototype runs 
just fine on one machine with 300 players, you need your production 
release to handle a thousand players, and you're already hovering near a 
quadratic bottleneck, you will need to scale your hardware by a factor 
10 to achieve a tripling of the playerbase. If you want to run the game 
with 3,000 players, you'll need a cluster of one hundred machines.

In reality, this means that a server that uses linear-time 
implementations of fundamental operations will never outgrow the 
prototype stage.

So what do you do?

> I'm also wondering if it would be ok to have an array of arrays storing 
> the clones:
> ({ ({ a_lot_of_clones }), ({ some_more_clones })... })
> 
> Are there any reasons not to do it that way?

That's a possibility, and avoids the full O(N^2) problem, but it's kind 
of clunky. I'd suggest using mappings of mappings for virtually every 
place you store a significant list of anything. We have long since 
written special-purpose LWO's for the Skotos mudlib (called bigmaps) 
that do the grunt work of maintaining sets.

Thus clone-maintenance code looks something like:

   object clones;

   static void create() {
     clones = new_object("/data/ob_bigmap");
   }

   atomic void add_clone(object clone) {
     clones->add(clone);
   }
   atomic void del_clone(object clone) {
     clones->del(clone);
   }
   object iterator() {
     return clones->iterator();
   }


while ob_bigmap.c looks something like (this is from memory, it will 
probably not compile) --

   mapping mapofmaps;

   static void create() {
     mapofmaps = ([ ]);
   }
   private atomic mapping get_row(object ob) {
     mapping row;
     int ix;

     /* I kind of wish status(ob) returned the clone number */
     if (sscanf(object_name(ob), "%*s#%d", ix) != 1) {
       ix = status(ob)[O_INDEX];
     }
     row = mapofmaps[ix / 1024];
     if (row == nil) {
       row = mapofmaps[ix / 1024] = ([ ]);
     }
     return row;
   }

   atomic void add(object ob) {
     get_row(ob)[ob] = TRUE;
   }
   atomic void del(object ob) {
     get_row(ob)[ob] = nil;
   }
   object iterator() {
     return new_object("/data/bigmap_iterator", mapofmaps);
   }


I will leave the iterator as an excercise for the reader. :)

Zell

From: dgd at list.imaginary.com (Par Winzell)
Date: Sat Apr  3 15:42:11 2004
Subject: [DGD] Clones and very large arrays

> I'm using a linked list.  This distributes the data across more objects (to
> mitigate swapping concerns) and avoids any issues with respect to max array
> size.  Though obviously getting the actual list of all clones isn't anywhere
> near as fast as it would be by your method.  I don't find a need to get that
> information too often, though.

The problem with this approach is that in my experience, minimizing 
swapping is your primary concern, especially now that call_touch() 
actually enables us to affect objects without swapping them in. If you 
use a linked list where the next/prev pointers are stored in the actual 
objects, then to even enumerate all the clones of a clonable, you have 
to swap them all in. Swapping a quarter of a million objects into memory 
is not realistic, and if you do it lazily (i.e. try not to freeze your 
game) it takes hours and hours.

Zell

From: DGD Mailing List (Par Winzell)
Date: Sun Apr  4 09:16:05 2004
Subject: [DGD] Re: Clones and very large arrays

Robert Forshaw wrote:

> Thanks for that, it was very insightful. I have a few questions.
> 
> First of all, the following line:
> 
> get_row(obj)[obj] = TRUE;
> 
> seems to suggest that mappings are passed by reference! Is this the 
> case? I wasn't aware of it.

Yes, they are, at least to some extent. I believe the contract with the 
driver is that references last as long as you stay either inside one 
thread or inside one object. In other words, if you store a reference in 
one object, export it to another, and the thread ends, DGD reserves the 
right to create a private copy of the mapping/array in the other object. 
  So you can't keep long-term references to data inside another object.


> Secondly, I understood everything in the code except the iterator. I 
> know what an iterator is, but that's about it. I wouldn't mind knowing 
> what was in /data/bigmap_iterator, but you haven't revealed that. I also 
> have no idea what the second argument to new_object is supposed to be, 
> as the driver version doesn't have one.

We redefined new_object() in our system-level auto object for 
convenience; it reads something like (again from memory) --

static atomic
object new_object(mixed ob, mixed args...) {
    ob = ::new_object(ob);
    /*
    ** actually this is a simplification; we relay the call through a
    ** _F_configure() function to allow configure() to be static
    */
    ::call_other(ob, "configure", args...);
    return ob;
}

> Thirdly, why 1024? As opposed to any other number.

No particular reason. It means you assume nobody is ever going to set 
DGD's max-array-size parameter lower than 1024, and it means that you 
can store a total of max-array-size * 1024 objects. You could make it 
e.g. 4096 if you preferred. If you mean 1024 rather than e.g. 1000, 1024 
is just a rounder number (2^10).

As for the iterator, there's a lot of ways you could implement it. There 
are two basic kinds of iterators in the world; one that promises it will 
capture the state of the data-structure when iterator() is called and so 
even if the data structure itself changes later, the iterator will still 
enumerate precisely what was there when the snapshot was made...

... and one kind of iterator that makes no such promise. Obviously the 
former kind has to either use more memory, or special tricks. If you 
don't mind the iterator using a fair bit of memory (temporarily) you can 
  simply let the iterator LWO make a private copy of the bigmap, keep 
two index counters (one to index the mapping of mappings, one to index 
the current inner mapping), and step through the whole thing step by 
step as next() is called.

I stole Sun's most recent interface pretty much directly;

   int has_next();
   mixed next();
   int size();


The nice bit about generalized iterators is that you are free to vary 
your internal implementation both of the datastructure and of the 
iterator as you please. All the caller cares about is being returned an 
object that keeps track of the iteration state. For example, the Skotos 
all-clones-of-a-certain-clonable data structure uses a SINGLE mapping 
only. If the number of clones of a clonable grows beyound 1024 or so 
(maybe we use 2048 or 4096), we just empty the data structure (!). Why? 
Because DGD offers a second method to enumerate all the clones of a 
given clonable, namly find_object().

Thus, for clonables with lots of clones, we return an iterator that 
reads basically:


string clonable;
object next;
int ix;

static atomic
void configure(string str) {
   clonable = str;
   ix = 1; forward();
}

int has_next() {
   return !!next;
}

object next() {
   if (next) {
     object ob;

     ob = next;
     forward();

     return ob;
   }
   error("read past end");
}

private
void forward() {
   while (ix <= status()[ST_OTABSZ]) {
     next = find_object(clonable + "#" + ix);
     ix ++;

     if (next) {
       return next;
     }
   }
   next = nil;
}

From: Erwin Harte
Subject: [DGD] Big, bigger... biggest?
Date: Wed Jan  5 02:55:01 2005

Hello,

About 2-3 months ago the Castle Marrach game at Skotos
(www.skotos.net) broke the 2GB barrier, with its statedump currently
at 525394 sectors of 4k each:

    > code 525394 * 4 * 1024
    $40 = -2142953472

:-)

Are there any other big DGD-using games out there, or does that make
it the biggest DGD-based game in existence?

Cheers,

Erwin.
P.S: Good to see the list back up and running. :)
-- 
Erwin Harte

Back to top level