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 |