Inleiding
Op 20 februari 2009 introduceerden we een snellere Pricewatch-engine, wat leidde tot aanzienlijke prestatieverbeteringen voor de overzichten van producten binnen een categorie. Dat was bij de introductie overigens ook de enige functie ervan. De nieuwe omgeving was in de vorm van een Java Servlet opgezet en werd via get-requests uitgelezen, vergelijkbaar met REST.
Naast en door die prestatieverbeteringen werd het ook mogelijk om de functionaliteit van de Pricewatch verder uit te breiden. Zo werden de intervallen bij de prijsfiltering gebruiksvriendelijker gemaakt en werd met Pricewatch 3.0 ook de zoekmachine voor producten erin overgebracht. Daarnaast kwamen toen ook de mooiere 'productspecificatiesamenvattingen' onder de productnaam en kwamen er 'berekende specificaties', zoals x euro per gigabyte bij harde schijven. Voor die laatste twee hebben we zelfs een dsl geïntroduceerd, zodat het beheer ervan bij onze contentmedewerkers kan blijven en niet door developers gedaan hoeft te worden.
Pricewatch-engine uitgebreid tot algemene engine
Tijdens het ontwerpen van Tweakers 7.0 werd besloten dat we de presentatie van lijsten zoals die van de Pricewatch overal zouden gaan doorvoeren. Alle lijsten, bijvoorbeeld van nieuws, reviews en video's, moesten voorzien worden van facetten en filters met de van de Pricewatch bekende, dynamische filteropties. Daarnaast wilden we graag de facetten uitbreiden met een indicatie van hoeveel resultaten eraan voldoen, zodat je als gebruiker minder hoeft te gokken. Bovendien moesten deze facetten hiërarchisch werken; als een redacteur het 'Crucial m4'-product koppelt aan zijn artikel, dan moeten bij de facetten ook het merk Crucial en de ssd-categorie getoond worden. En omgekeerd, als iemand het facet Crucial of ssd-categorie kiest, moet dat artikel er ook bij staan.
De techniek voor het samenstellen en afleiden van de facetten zat op dat moment uiteraard al in die Pricewatch-engine. Ook alle informatie voor die hiërarchische afleiding van facetten aan de hand van gekozen producten was daarin al voorhanden. Daarom kozen we er destijds voor om diezelfde aanpak, front-end in php en data/filter-back-end in Java, uit te breiden voor alle soorten informatie die we op een vergelijkbare manier wilden presenteren. Op enkele uitzonderingen na komt al onze content nu uit die engine. De meeste detailpagina's van artikelen en producten halen hun gegevens eruit, de meeste lijstjes van artikelen en producten komen ervandaan, zoekopdrachten worden erdoor uitgevoerd, enzovoort.
De engine faciliteert ook de directe en indirecte relaties tussen artikelen en onderwerpen. Als een redacteur een artikel bijvoorbeeld aan de Raptor-serie koppelt, dan weet de engine dat het ook over het merk Western Digital gaat.

Op het moment van schrijven is het Forum de belangrijkste uitzondering, maar ook die gaan we, in ieder geval gedeeltelijk, omzetten naar de nieuwe structuur. Dat heeft echter nogal wat voeten in aarde, waardoor we die stap niet gelijk hebben gemaakt. We wilden de introductie van Tweakers 7 niet ook daar nog van laten afhangen.
De keus om alles in een eigen Java-applicatie te bouwen leverde ons bij elke aankondiging weer nieuwe vragen op. Ook bij het artikel over Java-geheugengebruik werd er weer over gediscussieerd. Met dit artikel proberen we wat antwoorden op die vragen te geven.
Hoe werkt die engine dan?
Zonder dat je weet wat het doel van de engine is, is het uiteraard niet mogelijk om alternatieven te bespreken. Het beschrijven van alle details, performancetrucjes en algoritmes zou dit artikel veel te lang maken, maar hier volgt een samenvatting.
Doel van onze engine
In de praktijk ondersteunt de engine veel van de pagina's in Tweakers 7. Zo wordt de categorieboom die je op de portal van de Pricewatch ziet staan op veel pagina's intern gebruikt. Het gaat dus niet alleen om gegevens die uiteindelijk voor gebruikers zichtbaar worden. In algemene zin geldt dat de engine vooral gebruikt wordt voor het teruggeven van 'item'-lijstjes volgens specifieke criteria, sortering en paginering. Items zijn in deze context bijvoorbeeld onze nieuwsartikelen, reviews, producten of merken. Daarnaast kan de engine informatie afleiden van de in ram opgeslagen items, en daar nieuwe lijstjes en samenvattingen van teruggeven.
Zo'n lijstje kan ook slechts één item bevatten, zodat het gebruikt kan worden voor een detailpagina van een artikel, bijvoorbeeld de pagina die je nu leest. Een lijstje items is echter vaker een zoekopdracht die al dan niet binnen een specifieke context moet plaatsvinden. Denk aan een zoekopdracht voor alle V&A-advertenties met de tekst 'Asus' in de tabletcategorie, waarbij de vraagprijs lager dan 400 euro is. Andere voorbeelden zijn op veel plaatsen te vinden, zoals de nieuwsberichten, reviews, V&A-advertenties en producten. Ook de lijstjes producten en gerelateerde artikelen naast een nieuwsbericht zijn voorbeelden.
Veel pagina's worden opgebouwd uit verschillende engine calls, per lijstje een. Op het moment van schrijven werden er op de reviewportal 11 engine calls gedaan, op de V&A-portal 5 en op de hierboven gelinkte productentab 2. Ook op de pagina die je nu voor je ziet, werden diverse calls gedaan. Onder andere voor het ophalen van het artikel zelf, de categorieboom, de gerelateerde onderwerpen en de gerelateerde artikelen.
In de afbeelding hieronder zie je waar de engine calls op de reviewportal worden gedaan. Iedere aanroep levert genoeg informatie op voor de php-code om zich vooral op de weergave te kunnen richten.

Naast lijstjes van items worden ook stukken afgeleide informatie verzameld en teruggegeven. De afgeleide informatie is bijvoorbeeld de informatie die nodig is om te bepalen welke categorieën we moeten tonen in een 'categoriekiezer'. Je wilt tenslotte niet dat er in de V&A-categoriekiezer een categorie wordt getoond die geen advertenties bevat. Ook de populaire onderwerpen onder aan de pagina zijn een vorm van die afgeleide informatie.
Opzet van de engine
De engine is effectief een in-memory objectdatabase die specifiek ontwikkeld is om (veel van) de informatie die je op Tweakers tegenkomt te filteren, sorteren en klaarstomen voor presentatie en om facetten van de resultaten te onttrekken. Bij die informatie horen ook de relaties tussen artikelen onderling en met de tweakbase-entiteiten zoals Categorie, Merk, Serie en Tag.
De engine doet al dat werk zo veel mogelijk op een manier die aansluit bij wat de php-code verwacht en hoe de desbetreffende lijstjes werken. Het is overigens niet de bedoeling dat de engine zich bezighoudt met de presentatie van informatie; dat laten we zo veel mogelijk over aan de php-code.
De informatie op Tweakers is in diverse databronnen opgeslagen. De meeste data zit uiteindelijk in MySQL, de informatie van je bezoekerssessie zit in MongoDB en de multimediabestanden staan op de harde schijf. Om die data efficiënt te kunnen gebruiken wordt sommige informatie in Memcached gecached of wordt bij het opslaan gebruikgemaakt van een Message Queue via ActiveMQ.

Daarnaast speelt onze engine uiteraard een belangrijke rol bij het efficiënt ophalen en verwerken van gegevens. Uiteindelijk haalt de engine die gegevens echter weer domweg uit MySQL en bewaart hij een kopie van die data in 'native' Java-objecten in zijn ram-geheugen. De engine draait als een Servlet binnen Tomcat; het toeval wil dat we met de overstap naar Tweakers 7 ook overgingen op Java 7 en Tomcat 7 
De engines houden die data synchroon met wat er in MySQL staat. Dit doen ze zowel door zelf periodiek, bijvoorbeeld elke vijf minuten, de gegevens te verversen én door te 'luisteren' op verschillende JMS-Topics in ActiveMQ. Bij wijzigingen worden daar dan vanuit php berichten naartoe gestuurd, zodat het voor de php-code niet nodig is om te weten hoeveel engines er draaien en of die wel of niet geïnteresseerd zijn in informatie over de specifieke wijziging.
We draaien momenteel namelijk zes instanties, op iedere webserver een, zodat de communicatie daarmee lekker snel verloopt, met het tcp-verkeer over lokale netwerkpoorten. Dat zou zonder die JMS-Topics echter ook betekenen dat de php-code dan naar zes instanties moet verbinden om te melden dat er wat veranderd is. ActiveMQ kan dat een stuk efficiënter en zorgt ervoor dat de php-code kan doorgaan ten behoeve van de bezoeker die op dat moment zit te wachten.
Een pageview komt uiteraard binnen op een van onze loadbalancers. Wordt daarna meestal doorgestuurd naar een Varnish reverse proxy en die stuurt hem dan, als hij niet in zijn eigen cache zit, door naar Apache. De php-code die binnen Apache de request afhandelt, vergaart vervolgens alle benodigde gegevens uit MongoDB, Memcached, MySQL en de engine, en genereert daarmee de html die, via Varnish en de loadbalancer, wordt teruggestuurd naar je browser.

Sinds de introductie van Tweakers 7 doen we voor alle pageviews samen gemiddeld 4,8 queries per pageview op onze databases (zowel MySQL en MongoDB), 3,8 queries op Memcached en 3,9 engine calls. De meeste van die 4,8 queries gaan naar MongoDB om je sessie-informatie op te halen en bij te werken. Memcached wordt veel gebruikt voor eenvoudige stukjes informatie, zoals het aantal reacties op een artikel, en de engine uiteindelijk voor de belangrijkste stukken informatie op een pagina.
Een engine call is domweg een REST-operatie (nou ja, we doen alleen http get) met diverse parameters om aan te geven op welke manier de gegevens moeten worden opgezocht, gefilterd, gesorteerd en gepagineerd. Om het werk van de engine zo efficiënt mogelijk te doen wordt de lijst met mogelijke artikelen gesorteerd bewaard, vertaald in bitsets en alleen opnieuw gesorteerd als sortering relevant was en/of anders dan de standaardsortering. De engine handelt voor de meeste calls deze stappen af:
- vertaal get-parameters in een filtersettings-object;
- bepaal basisbitset voor de objecten, bijvoorbeeld een zoekopdracht, alle producten in een categorie of alle objecten uit een lijstje met id's;
- pas overige filtering/facetten toe, bijvoorbeeld alleen producten onder de 400 euro en van het merk Scythe;
- indien nodig, verzamel beschikbare facetten en aantallen per filter met in achtneming van and- of or-instructie;
- sorteer het resultaat voor als niet de standaardsortering gebruikt moet worden;
- pagineer het resultaat voor zover nodig;
- vertaal Java-objecten in php-serialized of php's igbinary encoding;
- verstuur het resultaat naar de php-kant.
De stappen 2, 3 en eventueel 4 zijn dingen die je misschien van Lucene of Solr herkent. De werking is ook vergelijkbaar, met dien verstande dat onze aanpak uiteraard specifiek voor Tweakers is geschreven. Bovendien worden onze objecten niet in 'documenten' vertaald, wat een reeks vertaalslagen tussen allerlei stukken geheugen bespaart.
Doordat de stappen 1 tot en met 6 doorgaans snel klaar zijn, zit in de praktijk de meeste tijd in de stappen 7 en 8. Voor veel requests loopt dat op tot 90% van de tijd. Gelukkig kunnen we die tijden nog steeds in enkele milliseconden uitdrukken, maar het genereren van alle binaire of tekstuele output om de serialized representatie van php-objecten te genereren is helaas niet gratis. De alternatieven zijn echter niet beter; het is in de praktijk veel sneller dan bijvoorbeeld het generen van xml en nauwelijks trager dan er Json of vergelijkbare compacte datarepresentaties van te maken. Bovendien is het aan de php-zijde terugvertalen van xml, Json of andere opties een stuk duurder dan domweg unserialize aan te kunnen roepen. In de praktijk bleek dat we beter een beetje meer werk aan de Java-zijde konden doen dan php opzadelen met een complexe vertaalslag.
Waarom niet oplossing X?
De belangrijkste vraag die we krijgen, is waarom we eigenlijk de moeite hebben genomen een eigen omgeving te schrijven. Er worden daarbij allerlei moderne platforms genoemd, maar er wordt vergeten dat toen we met dit project begonnen het landschap van complexe zoekplatforms er heel anders uitzag.
Dat kan toch gewoon in SQL?
Een van de eerste vragen is iets in de geest van: "Dat kan toch gewoon in SQL?" Het korte antwoord is: "Volgens ons niet." Het uitvoeren van de filtering op zichzelf (prijs > 100 euro en < 1000 euro, enzovoort) bestaat in de basis uit set-operaties en is daarmee perfect naar SQL-statements te vertalen. De hoeveelheid gegevens waar het om gaat en de aantallen facetten waarmee het moet werken zijn echter niet erg geschikt voor SQL.
Je kan uiteraard werken met temporary tables voor het opslaan van tussenresultaten, maar uiteindelijk gaat de complexiteit van de statements ten koste van de performance. Bovendien was MySQL 5.1 in die tijd net uit, wat betekent dat we nog op 5.0 draaiden en die stond niet bekend om zijn performance met complexe queries en queries met subqueries. Met zaken als de parent-childrelaties van categorieën en 'natural order'-sortering wordt het allemaal nog spannender.
Waarom geen Solr of ElasticSearch?
Twee veel genoemde platforms zijn Solr en ElasticSearch. In februari 2009 was Solr 1.3 echter nog maar net uit en ElasticSearch lijkt pas voor het eerst publiekelijk te zijn aangekondigd in februari 2010. Ook andere moderne 'NoSQL'-omgevingen stonden toen in hun kinderschoenen of waren nog niet publiekelijk aangekondigd. Bovendien geldt voor een omgeving die is aangekondigd nog niet dat wij die ook kennen 
Ook toen we konden kiezen tussen uitbreiding van onze eigen omgeving voor de Pricewatch naar een generiek platform of het compleet vervangen ervan door een 'off-the-shelf'-product, kozen we voor het eerste. Tegen die tijd hadden we allerlei features ingebouwd waarvan we geen praktisch equivalent zagen in, met name, Solr.
Nadelen van het documentmodel
Behalve dat ze functionaliteit missen, zijn de meeste NoSQL-omgevingen en zoekmachines volgens het document-storage-model opgebouwd. Dat betekent dat alle informatie gedenormaliseerd opgeslagen wordt, alle informatie die relevant is voor een document wordt bij dat document opgeslagen. En het is doorgaans ook niet mogelijk of eenvoudig om informatie uit andere of andere typen documenten erbij te betrekken.
In het schema hieronder zie je een nieuwsbericht met een aantal van de relaties in een eenvoudige object graph en een vergelijkbaar gedenormaliseerd document. Hierbij zijn de losse documenten voor de producten, serie, merken en categorieën nog weggelaten. In de praktijk kan er natuurlijk ook gekozen worden om niet de namen te kopiëren, maar om die via een nieuwe document-look-up los op te halen.

Dat maakt het opzoeken van documenten eenvoudig; alle relevante informatie is tenslotte direct voorhanden. Zodra er echter iets bijgewerkt moet worden, is dat een ander verhaal. Dan moet je alle plekken waar die informatie gekopieerd was ook aanpassen. Als bijvoorbeeld de categorie Games een nieuwe naam krijgt, zouden alle producten, nieuwsberichten en reviews aangepast moeten worden om die nieuwe naam actief te krijgen. In het objectmodel hoeft alleen dat ene object aangepast te worden.
Daarnaast betekent het documentmodel bij Solr, ElasticSearch en andere dat je, als er ook maar één elementje verandert, het hele document opnieuw moet opbouwen, de oude elementen moet verwijderen en de nieuwe moet invoegen. En dat kan een nieuwe reeks pijnpunten opleveren.
In ons geval is de sortering op populariteit een goed voorbeeld. Om op populariteit te kunnen sorteren moet er ergens een cijfertje bestaan dat aangeeft hoe populair een item is. Bij het documentmodel moet dat in het document opgeslagen zijn, maar de informatie over de populariteit verandert uiteraard doorlopend. In onze SQL-database werken we die informatie elke tien minuten bij en daar is het dan een eenvoudig update-statement per tabel dat slechts één kolom hoeft aan te passen. In onze engine is het ook simpelweg voldoende om van alle relevante objecten één veld aan te passen en de waarde van een integerveld aanpassen is zo'n beetje het snelste wat er is in een computer.
Bij Solr zou je alle documenten opnieuw moeten indexeren, alleen maar om de populariteitsindicatie bij te werken. Met meer dan 500.000 documenten is dat iets wat je graag voorkomt, want het zou minuten kosten.
Waarom niet oplossing X?
Het komt erop neer dat veel off the shelf-alternatieven uiteindelijk niet (automatisch) beter zijn. Ze leveren zelfs niet per se minder werk op. Als we met Solr of ElasticSearch aan de slag hadden gewild, hadden we alsnog een groot deel zelf moeten programmeren, maar dan in de vorm van 'custom search components', 'custom analyzers' en al dat soort aspecten. Hoewel we nu veel in een Java-laag hebben, zouden we het dan wellicht meer in onze php-code hebben verwerkt. Is zo'n oplossing dan beter of slechter? Dat kun je niet zomaar zeggen; het is vooral anders. Er zijn in elk geval nieuwe voor- en nadelen, waardoor het niet domweg als verbetering kan worden gezien.
Daarnaast betekent het feit dat iemand nu aan oplossing X denkt niet dat wij die oplossing destijds ook kenden. Sterker, veel van dergelijke oplossingen bestonden nog niet toen wij met de eerste versie van de engine begonnen en waren gedurende de tijd van Tweakers 7 nog niet bekend of compleet. Verder geldt voor bijna al het niet-maatwerk dat je alsnog een deel maatwerk moet ontwikkelen. Is het niet om een en ander met elkaar samen te laten werken, dan is het wel omdat we alsnog specifieke eisen moeten invullen. De techniek moet immers het vervullen van de wensen zo min mogelijk in de weg staan.
Waarom in Java?
Behalve over onze keuze voor een oplossing, krijgen we ook vragen over het gekozen platform. We hebben gekozen voor een Servlet in Java. In 2008 was dat een prima en gebruikelijke oplossing om een REST-achtige omgeving op te zetten en wat ons betreft is het dat nog steeds. Uiteraard zijn er allerlei andere platforms en talen waarin hetzelfde had gekund, maar is dat ook beter of alleen anders?
Waarom niet gewoon in php zelf?
Zoals gezegd waren we in 2008 begonnen met de engine voor de Pricewatch. Destijds was ons duidelijk dat de php-code niet overweg kon met de hoeveelheid data die nodig was om een uitgebreide categorielisting te filteren, sorteren en samen met zijn facetten te presenteren. Het belangrijkste probleem zat hem in de complexiteit van die stappen samen; daardoor leek een pure SQL-oplossing onhaalbaar.
Met de eerste versie van de Pricewatch, met los instelbare specificaties per product, werd ons duidelijk dat de php-code behoorlijk wat gegevens moest ophalen. Die gegevens werden uit de database opgehaald en vertaald naar objecten, en dat kostte veel tijd. Om die tijd te besparen werden de objecten in Memcached opgeslagen. Ook het opslaan van objectbomen in Memcached bracht echter problemen met zich mee; veel van die data was namelijk groter dan 2MB, meer dan de maximale bucketgrootte van Memcached. Bovendien werd de objectboom, na unserialization, in php alsnog enorm. We zaten makkelijk over de 70MB aan ram-gebruik voor een paar duizend producten en dan hadden we nog niets gefilterd of gesorteerd.

Al met al kostten dergelijke handelingen na veelvuldig tunen nog altijd vaak meer dan een seconde, veel te lang voor onze eisen. Wij streven ernaar om de volledige pagina binnen 0,1 seconde te genereren!
Kortom, php viel af. Het belangrijkste knelpunt was dat er geen praktische manier was om de gegevens die uit de database of Memcached kwamen in ram vast te houden. Uiteindelijk bleek de Java-versie van diezelfde code zo veel sneller dat die inclusief de benodigde http-communicatie nog altijd veel minder tijd nodig had.
Waarom niet in taal X, die is toch veel beter dan Java?
Java is niet alleen een taal, het is een platform met diverse handige mogelijkheden. Zo is Tomcat een uitstekende Java Servlet-engine, terwijl de Servlet-technologie erg geschikt is voor onze toepassing. Daarnaast brengt de keuze voor Java een grote hoeveelheid bibliotheken met zich mee, zoals Lucene, Antlr, BCEL en Spring. Wat de taalkeuze op zich betreft komen we bovendien terug op de vraag die we ook stelden bij alternatieve oplossingen en databases: was het er al in 2008? En als het er al was, was het destijds net zo bekend en uitgebreid als het nu is? Kortom: was het in 2008 ook een goed alternatief?
Veel alternatieven beloven een betere productiviteit dan Java, maar als je het gebruik van een goede IDE meerekent, zijn de productiviteitsvoordelen van het alternatieve platform dan nog relevant? Een groot deel van het typewerk dat je in Java meer moet doen dan in andere platforms op de JVM valt immers weg door allerlei gradaties van autocompletion, code generation en short-keys. Denk daarbij aan het automatisch genereren van getters en setters, en het automatisch plaatsen van import-statements op het moment dat je het voorstel van de autocompletion accepteert.
Een ander sterk punt dat vaak genoemd wordt, bijvoorbeeld bij NodeJS en Scala, is de eenvoudige schaalbaarheid doordat allerlei werk asynchroon wordt gedaan. Dat levert echter vooral horizontale schaalbaarheid op, terwijl ook met de verticale schaalbaarheid rekening gehouden moet worden. Anders gezegd: als het alternatief wel meer requests tegelijk aankan (hogere concurrency), maar de performance vervolgens per stuk langzamer is (hogere latency en/of lagere througput) is het voor ons nog steeds geen goede oplossing.
Ironisch genoeg hadden we bij de introductie van Tweakers 7 inderdaad een schaalbaarheidsprobleem, dat echter niet door Tomcat 7 of Java kwam. We openden zo veel tcp-sockets naar de interne REST-service dat we uiteindelijk over de standaardgrens gingen van het aantal adressen dat Linux kan alloceren. Onze php-code noch onze Tomcat was dus de bottleneck en een asynchrone omgeving had hier geen winst opgeleverd.
Wat ons betreft was Java destijds een prima keus. Sterker, als we nu opnieuw moesten beginnen zou Java alsnog veel kans maken. We hebben nu eenmaal Java-kennis in huis en het is een uitgebreid platform met een scala aan bibliotheken en tools. Denk bijvoorbeeld aan de uitgebreide ide's en profilers die voor Java bestaan. Zijn er vergelijkbare tools voor de alternatieve platforms?
Nog een laatste punt: kan het alternatieve platform overweg met een paar gigabyte aan gegevens in ram? En als je dat op je platform hebt gestart, blijft het dan ook maandenlang stabiel draaien? Dat is namelijk wel onze ervaring met de Java-omgeving die we binnen Tomcat hebben draaien 
De engine in de toekomst
Onze site, code en engine kunnen altijd beter. De belangrijkste toepassing die nog niet gerealiseerd was bij de release van Tweakers 7, was de integratie van het forum. We willen namelijk dezelfde techniek gaan gebruiken om lijstjes forumtopics te kunnen presenteren, bijvoorbeeld als tab binnen een merkpagina. Die toont dan alle forumtopics die gekoppeld zijn aan het merk Kingston of producten van dat merk. Bovendien moet de zoektechniek die we voor veel andere onderdelen van de site hebben geïntroduceerd ook voor het forum gebruikt gaan worden. Omdat het hier gaat over tientallen gigabytes aan informatie, hebben we dit niet gelijk geprobeerd te integreren.

Op deze manier konden we eerst de basisideeën van de techniek goed in de praktijk testen. Bovendien zou het integreren van die functionaliteit onze overstapdatum weer weken of zelfs maanden uitgesteld hebben. Het is natuurlijk jammer voor degenen die al heel lang wachten op een betere zoekmachine in het forum, maar hij is eindelijk in ontwikkeling. Op het moment van schrijven is er zelfs al een goed werkende opzet, die we nu verder uitwerken 
/i/1351958625.png?f=imagemedium)
Daarnaast is het de bedoeling dat je de forumtopics ook bij de algemene zoekresultaten gaat vinden. Ook dit is geen triviale uitbreiding; dus ga er maar vanuit dat we de nieuwe forumzoekmachine eerst in gebruik nemen en dat we de geïntegreerde zoekfunctie pas in een latere iteratie uitbreiden.
Verder zullen we natuurlijk nog kijken naar andere onderdelen van de site die hier nog niet in opgenomen zijn en daar wel baat bij hebben. Momenteel vallen onder andere de Meuktracker, onze banensectie en wat andere kleinere delen nog (deels) buiten de boot. Ook die stonden eerder wel op het programma, maar zijn uiteindelijk uitgesteld om het Tweakers 7-project een gezonde einddatum te kunnen geven.