Browse Source

View: Use tree-structure to prevent duplicates in browsing

Julius Pfrommer 5 years ago
parent
commit
9789cdcdb6
3 changed files with 498 additions and 226 deletions
  1. 6 1
      src/server/ua_server_internal.h
  2. 24 12
      src/server/ua_services_nodemanagement.c
  3. 468 213
      src/server/ua_services_view.c

+ 6 - 1
src/server/ua_server_internal.h

@@ -251,8 +251,13 @@ compatibleDataType(UA_Server *server, const UA_NodeId *dataType,
 UA_Boolean
 compatibleValueRanks(UA_Int32 valueRank, UA_Int32 constraintValueRank);
 
+struct BrowseOpts {
+    UA_UInt32 maxReferences;
+    UA_Boolean recursive;
+};
+
 void
-Operation_Browse(UA_Server *server, UA_Session *session, const UA_UInt32 *maxrefs,
+Operation_Browse(UA_Server *server, UA_Session *session, const struct BrowseOpts *maxrefs,
                  const UA_BrowseDescription *descr, UA_BrowseResult *result);
 
 UA_DataValue

+ 24 - 12
src/server/ua_services_nodemanagement.c

@@ -357,8 +357,10 @@ findChildByBrowsename(UA_Server *server, UA_Session *session,
 
     UA_BrowseResult br;
     UA_BrowseResult_init(&br);
-    UA_UInt32 maxrefs = 0;
-    Operation_Browse(server, session, &maxrefs, &bd, &br);
+    struct BrowseOpts bo;
+    bo.maxReferences = 0;
+    bo.recursive = false;
+    Operation_Browse(server, session, &bo, &bd, &br);
     if(br.statusCode != UA_STATUSCODE_GOOD)
         return br.statusCode;
 
@@ -521,8 +523,10 @@ copyAllChildren(UA_Server *server, UA_Session *session,
 
     UA_BrowseResult br;
     UA_BrowseResult_init(&br);
-    UA_UInt32 maxrefs = 0;
-    Operation_Browse(server, session, &maxrefs, &bd, &br);
+    struct BrowseOpts bo;
+    bo.maxReferences = 0;
+    bo.recursive = false;
+    Operation_Browse(server, session, &bo, &bd, &br);
     if(br.statusCode != UA_STATUSCODE_GOOD)
         return br.statusCode;
 
@@ -1015,8 +1019,10 @@ recursiveCallConstructors(UA_Server *server, UA_Session *session,
 
     UA_BrowseResult br;
     UA_BrowseResult_init(&br);
-    UA_UInt32 maxrefs = 0;
-    Operation_Browse(server, session, &maxrefs, &bd, &br);
+    struct BrowseOpts bo;
+    bo.maxReferences = 0;
+    bo.recursive = false;
+    Operation_Browse(server, session, &bo, &bd, &br);
     if(br.statusCode != UA_STATUSCODE_GOOD)
         return br.statusCode;
 
@@ -1384,8 +1390,10 @@ recursiveDeconstructNode(UA_Server *server, UA_Session *session,
 
     UA_BrowseResult br;
     UA_BrowseResult_init(&br);
-    UA_UInt32 maxrefs = 0;
-    Operation_Browse(server, session, &maxrefs, &bd, &br);
+    struct BrowseOpts bo;
+    bo.maxReferences = 0;
+    bo.recursive = false;
+    Operation_Browse(server, session, &bo, &bd, &br);
     if(br.statusCode != UA_STATUSCODE_GOOD)
         return;
 
@@ -1420,8 +1428,10 @@ recursiveDeleteNode(UA_Server *server, UA_Session *session,
 
     UA_BrowseResult br;
     UA_BrowseResult_init(&br);
-    UA_UInt32 maxrefs = 0;
-    Operation_Browse(server, session, &maxrefs, &bd, &br);
+    struct BrowseOpts bo;
+    bo.maxReferences = 0;
+    bo.recursive = false;
+    Operation_Browse(server, session, &bo, &bd, &br);
     if(br.statusCode != UA_STATUSCODE_GOOD)
         return;
 
@@ -1856,8 +1866,10 @@ UA_Server_addMethodNodeEx_finish(UA_Server *server, const UA_NodeId nodeId, UA_M
 
     UA_BrowseResult br;
     UA_BrowseResult_init(&br);
-    UA_UInt32 maxrefs = 0;
-    Operation_Browse(server, &server->adminSession, &maxrefs, &bd, &br);
+    struct BrowseOpts bo;
+    bo.maxReferences = 0;
+    bo.recursive = false;
+    Operation_Browse(server, &server->adminSession, &bo, &bd, &br);
 
     UA_StatusCode retval = br.statusCode;
     if(retval != UA_STATUSCODE_GOOD) {

+ 468 - 213
src/server/ua_services_view.c

@@ -2,7 +2,7 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. 
  *
- *    Copyright 2014-2017 (c) Fraunhofer IOSB (Author: Julius Pfrommer)
+ *    Copyright 2014-2019 (c) Fraunhofer IOSB (Author: Julius Pfrommer)
  *    Copyright 2014-2017 (c) Florian Palm
  *    Copyright 2015-2016 (c) Sten Grüner
  *    Copyright 2015 (c) LEvertz
@@ -18,22 +18,161 @@
 
 #include "ua_server_internal.h"
 #include "ua_services.h"
+#include "ziptree.h"
+
+/***********/
+/* RefTree */
+/***********/
+
+/* References are browsed with a minimum number of copy operations. A RefTree
+ * holds a single array for both the NodeIds encountered during (recursive)
+ * browsing and the entries for a tree-structure to check for duplicates. Once
+ * the (recursive) browse has finished, the tree-structure part can be simply
+ * cut away. A single realloc operation (with some pointer repairing) can be
+ * used to increase the capacity of the RefTree.
+ *
+ * If an ExpandedNodeId is encountered, it has to be processed right away.
+ * Remote ExpandedNodeId are not put into the tree, since it is not possible to
+ * recurse into them anyway.
+ *
+ * The layout of the results array is as follows:
+ *
+ * | Targets [NodeId] | Tree [RefEntry] | */
+
+#define UA_BROWSE_INITIAL_SIZE 16
+
+typedef struct RefEntry {
+    ZIP_ENTRY(RefEntry) zipfields;
+    const UA_NodeId *target;
+    UA_UInt32 targetHash; /* Hash of the target nodeid */
+} RefEntry;
+ 
+static enum ZIP_CMP
+cmpTarget(const void *a, const void *b) {
+    const RefEntry *aa = (const RefEntry*)a;
+    const RefEntry *bb = (const RefEntry*)b;
+    if(aa->targetHash < bb->targetHash)
+        return ZIP_CMP_LESS;
+    if(aa->targetHash > bb->targetHash)
+        return ZIP_CMP_MORE;
+    return (enum ZIP_CMP)UA_NodeId_order(aa->target, bb->target);
+}
+
+ZIP_HEAD(RefHead, RefEntry);
+typedef struct RefHead RefHead;
+ZIP_PROTTYPE(RefHead, RefEntry, RefEntry)
+ZIP_IMPL(RefHead, RefEntry, zipfields, RefEntry, zipfields, cmpTarget)
+
+typedef struct {
+    UA_NodeId *targets;
+    RefHead head;
+    size_t capacity; /* available space */
+    size_t size;     /* used space */
+} RefTree;
+
+static UA_StatusCode
+RefTree_init(RefTree *rt) {
+    size_t space = (sizeof(UA_NodeId) + sizeof(RefEntry)) * UA_BROWSE_INITIAL_SIZE;
+    rt->targets = (UA_NodeId*)UA_malloc(space);
+    if(!rt->targets)
+        return UA_STATUSCODE_BADOUTOFMEMORY;
+    rt->capacity = UA_BROWSE_INITIAL_SIZE;
+    rt->size = 0;
+    ZIP_INIT(&rt->head);
+    return UA_STATUSCODE_GOOD;
+}
+
+static
+void RefTree_clear(RefTree *rt) {
+    for(size_t i = 0; i < rt->size; i++)
+        UA_NodeId_deleteMembers(&rt->targets[i]);
+    UA_free(rt->targets);
+}
+
+/* Double the capacity of the reftree */
+static UA_StatusCode
+RefTree_double(RefTree *rt) {
+    size_t capacity = rt->capacity * 2;
+    size_t space = (sizeof(UA_NodeId) + sizeof(RefEntry)) * capacity;
+    UA_NodeId *newTargets = (UA_NodeId*)realloc(rt->targets, space);
+    if(!newTargets)
+        return UA_STATUSCODE_BADOUTOFMEMORY;
+
+    /* Repair the pointers for the realloced array+tree  */
+    uintptr_t arraydiff = (uintptr_t)newTargets - (uintptr_t)rt->targets;
+    RefEntry *reArray = (RefEntry*)
+        ((uintptr_t)newTargets + (capacity * sizeof(UA_NodeId)));
+    uintptr_t entrydiff = (uintptr_t)reArray -
+        ((uintptr_t)rt->targets + (rt->capacity * sizeof(UA_NodeId)));
+    RefEntry *oldReArray = (RefEntry*)
+        ((uintptr_t)newTargets + (rt->capacity * sizeof(UA_NodeId)));
+    memmove(reArray, oldReArray, rt->size * sizeof(RefEntry));
+    for(size_t i = 0; i < rt->size; i++) {
+        if(reArray[i].zipfields.zip_left)
+            *(uintptr_t*)&reArray[i].zipfields.zip_left += entrydiff;
+        if(reArray[i].zipfields.zip_right)
+            *(uintptr_t*)&reArray[i].zipfields.zip_right += entrydiff;
+        *(uintptr_t*)&reArray[i].target += arraydiff;
+    }
+
+    rt->head.zip_root = (RefEntry*)((uintptr_t)rt->head.zip_root + entrydiff);
+    rt->capacity = capacity;
+    rt->targets = newTargets;
+    return UA_STATUSCODE_GOOD;
+}
+
+static UA_StatusCode
+RefTree_add(RefTree *rt, const UA_NodeId *target) {
+    UA_StatusCode s = UA_STATUSCODE_GOOD;
+    if(rt->capacity <= rt->size) {
+        s = RefTree_double(rt);
+        if(s != UA_STATUSCODE_GOOD)
+            return s;
+    }
+    s = UA_NodeId_copy(target, &rt->targets[rt->size]);
+    if(s != UA_STATUSCODE_GOOD)
+        return s;
+    RefEntry *re = (RefEntry*)((uintptr_t)rt->targets +
+                               (sizeof(UA_NodeId) * rt->capacity) +
+                               (sizeof(RefEntry) * rt->size));
+    re->target = &rt->targets[rt->size];
+    re->targetHash = UA_NodeId_hash(target);
+    ZIP_INSERT(RefHead, &rt->head, re, ZIP_FFS32(UA_UInt32_random()));
+    rt->size++;
+    return UA_STATUSCODE_GOOD;
+}
+
+/*********************/
+/* ContinuationPoint */
+/*********************/
 
 struct ContinuationPoint {
     ContinuationPoint *next;
-    UA_ByteString identifier;
-    UA_BrowseDescription browseDescription;
+    UA_Guid identifier;
     UA_UInt32 maxReferences;
+    UA_BrowseDescription bd;
+    UA_Boolean recursive;
 
-    /* The last point in the node references? */
-    size_t referenceKindIndex;
-    size_t targetIndex;
+    RefTree rt;
+    size_t n;    /* Index of the node in the rt that is currently visited */
+    size_t nk;   /* Index of the ReferenceKind in the node that is visited */
+    size_t nki;  /* Index of the reference in the ReferenceKind that is visited */
 };
 
+static UA_StatusCode
+ContinuationPoint_init(ContinuationPoint *cp, UA_UInt32 maxRefs,
+                       UA_Boolean recursive) {
+    memset(cp, 0, sizeof(ContinuationPoint));
+    cp->identifier = UA_Guid_random();
+    cp->maxReferences = maxRefs;
+    cp->recursive = recursive;
+    return RefTree_init(&cp->rt);
+}
+
 ContinuationPoint *
 ContinuationPoint_clear(ContinuationPoint *cp) {
-    UA_ByteString_deleteMembers(&cp->identifier);
-    UA_BrowseDescription_deleteMembers(&cp->browseDescription);
+    UA_BrowseDescription_clear(&cp->bd);
+    RefTree_clear(&cp->rt);
     return cp->next;
 }
 
@@ -41,17 +180,72 @@ ContinuationPoint_clear(ContinuationPoint *cp) {
 /* Browse */
 /**********/
 
+/* The RefTree structure is kept across calls to Browse(Next). The RefResult
+ * structure is valid only for the current service call. Note that we can call
+ * Browse internally without a RefResult. Then, only the RefTree is set up with
+ * the target identifiers. */
+
+typedef struct {
+    size_t size;
+    size_t capacity;
+    UA_ReferenceDescription *descr;
+} RefResult;
+
+static UA_StatusCode
+RefResult_init(RefResult *rr, UA_UInt32 maxRefs) {
+    UA_UInt32 initialRes = UA_BROWSE_INITIAL_SIZE;
+    if(initialRes > maxRefs)
+        initialRes = maxRefs;
+    memset(rr, 0, sizeof(RefResult));
+    rr->descr = (UA_ReferenceDescription*)
+        UA_Array_new(initialRes, &UA_TYPES[UA_TYPES_REFERENCEDESCRIPTION]);
+    if(!rr->descr)
+        return UA_STATUSCODE_BADOUTOFMEMORY;
+    rr->capacity = initialRes;
+    rr->size = 0;
+    return UA_STATUSCODE_GOOD;
+}
+
+static UA_StatusCode
+RefResult_double(RefResult *rr, UA_UInt32 maxSize) {
+    size_t newSize = rr->capacity * 2;
+    if(newSize > maxSize)
+        newSize = maxSize;
+    UA_ReferenceDescription *rd = (UA_ReferenceDescription*)
+        UA_realloc(rr->descr, newSize * sizeof(UA_ReferenceDescription));
+    if(!rd)
+        return UA_STATUSCODE_BADOUTOFMEMORY;
+    memset(&rd[rr->size], 0, sizeof(UA_ReferenceDescription) * (newSize - rr->size));
+    rr->descr = rd;
+    rr->capacity = newSize;
+    return UA_STATUSCODE_GOOD;
+}
+
+static void
+RefResult_clear(RefResult *rr) {
+    UA_assert(rr->descr != NULL);
+    for(size_t i = 0; i < rr->size; i++)
+        UA_ReferenceDescription_clear(&rr->descr[i]);
+    UA_free(rr->descr);
+}
+
 /* Target node on top of the stack */
 static UA_StatusCode
-fillReferenceDescription(UA_Server *server, const UA_Node *curr,
-                         const UA_NodeReferenceKind *ref,
-                         UA_UInt32 mask, UA_ReferenceDescription *descr) {
-    UA_ReferenceDescription_init(descr);
-    UA_StatusCode retval = UA_NodeId_copy(&curr->nodeId, &descr->nodeId.nodeId);
+fillReferenceDescription(UA_Server *server, const UA_NodeReferenceKind *ref, UA_UInt32 mask,
+                         const UA_ExpandedNodeId *nodeId, const UA_Node *curr,
+                         UA_ReferenceDescription *descr) {
+    /* Fields without access to the actual node */
+    UA_StatusCode retval = UA_ExpandedNodeId_copy(nodeId, &descr->nodeId);
     if(mask & UA_BROWSERESULTMASK_REFERENCETYPEID)
         retval |= UA_NodeId_copy(&ref->referenceTypeId, &descr->referenceTypeId);
     if(mask & UA_BROWSERESULTMASK_ISFORWARD)
         descr->isForward = !ref->isInverse;
+
+    /* Remote references (ExpandedNodeId) are not further looked up here */
+    if(!curr)
+        return retval;
+    
+    /* Fields that require the actual node */
     if(mask & UA_BROWSERESULTMASK_NODECLASS)
         retval |= UA_NodeClass_copy(&curr->nodeClass, &descr->nodeClass);
     if(mask & UA_BROWSERESULTMASK_BROWSENAME)
@@ -89,255 +283,284 @@ matchClassMask(const UA_Node *node, UA_UInt32 nodeClassMask) {
     return true;
 }
 
-/* Returns whether the node / continuationpoint is done */
-static UA_Boolean
-browseReferences(UA_Server *server, const UA_Node *node,
-                 ContinuationPoint *cp, UA_BrowseResult *result) {
-    UA_assert(cp != NULL);
-    const UA_BrowseDescription *descr = &cp->browseDescription;
+static UA_StatusCode
+browseNodeRefKind(UA_Server *server, UA_Session *session, ContinuationPoint *cp,
+                  RefResult *rr, UA_Boolean *maxed, const UA_NodeReferenceKind *rk,
+                  const UA_ExpandedNodeId *target) {
+    /* Is the target already in the tree? */
+    RefEntry re;
+    re.target = &target->nodeId;
+    re.targetHash = UA_NodeId_hash(&target->nodeId);
+    if(ZIP_FIND(RefHead, &cp->rt.head, &re)) {
+        cp->nki++;
+        return UA_STATUSCODE_GOOD;
+    }
 
-    /* If the node has no references, just return */
-    if(node->referencesSize == 0) {
-        result->referencesSize = 0;
-        return true;
+    /* Get the target if it is not a remote node */
+    const UA_Node *node = NULL;
+    if(target->serverIndex == 0) {
+        node = UA_Nodestore_getNode(server->nsCtx, &target->nodeId);
+        if(node) {
+            /* Test if the node class matches */
+            UA_Boolean match = matchClassMask(node, cp->bd.nodeClassMask);
+            if(!match) {
+                UA_Nodestore_releaseNode(server->nsCtx, node);
+                cp->nki++;
+                return UA_STATUSCODE_GOOD;
+            }
+        }
     }
 
-    /* Follow all references? */
-    UA_Boolean browseAll = UA_NodeId_isNull(&descr->referenceTypeId);
+    /* A match! */
 
-    /* How many references can we return at most? */
-    size_t maxrefs = cp->maxReferences;
-    if(maxrefs == 0) {
-        if(server->config.maxReferencesPerNode != 0) {
-            maxrefs = server->config.maxReferencesPerNode;
-        } else {
-            maxrefs = UA_INT32_MAX;
-        }
-    } else {
-        if(server->config.maxReferencesPerNode != 0 &&
-           maxrefs > server->config.maxReferencesPerNode) {
-            maxrefs = server->config.maxReferencesPerNode;
+    /* Don't return detailed results if not required */
+    UA_StatusCode retval = UA_STATUSCODE_GOOD;
+    if(!rr)
+        goto cleanup;
+
+    /* Increase the capacity of the results array/tree if required */
+    if(rr->capacity <= rr->size) {
+        retval = RefResult_double(rr, cp->maxReferences);
+        /* Reached MaxReferences; Redo for this target */
+        if(retval != UA_STATUSCODE_GOOD)
+            goto cleanup;
+        if(rr->size == rr->capacity) {
+            *maxed = true;
+            goto cleanup;
         }
     }
 
-    /* Allocate the results array */
-    size_t refs_size = 2; /* True size of the array */
-    result->references = (UA_ReferenceDescription*)
-        UA_Array_new(refs_size, &UA_TYPES[UA_TYPES_REFERENCEDESCRIPTION]);
-    if(!result->references) {
-        result->statusCode = UA_STATUSCODE_BADOUTOFMEMORY;
-        return false;
-    }
+    /* Fill the detailed results */
+    retval = fillReferenceDescription(server, rk, cp->bd.resultMask, target,
+                                      node, &rr->descr[rr->size]);
+    if(retval != UA_STATUSCODE_GOOD)
+        goto cleanup;
+    rr->size++;
 
-    size_t referenceKindIndex = cp->referenceKindIndex;
-    size_t targetIndex = cp->targetIndex;
+ cleanup:
+    if(!(*maxed) && retval == UA_STATUSCODE_GOOD) {
+        /* Proceed to the next target */
+        if(node)
+            retval = RefTree_add(&cp->rt, &target->nodeId);
+        cp->nki++;
+    }
+    if(node)
+        UA_Nodestore_releaseNode(server->nsCtx, node);
+    return retval;
+}
 
-    /* Loop over the node's references */
-    for(; referenceKindIndex < node->referencesSize; ++referenceKindIndex) {
-        UA_NodeReferenceKind *rk = &node->references[referenceKindIndex];
+static UA_StatusCode
+browseNode(UA_Server *server, UA_Session *session,
+           ContinuationPoint *cp, RefResult *rr, UA_Boolean *maxed,
+           size_t referenceTypesSize, const UA_NodeId *referenceTypes,
+           const UA_Node *node) {
+    for(; cp->nk < node->referencesSize; cp->nk++, cp->nki = 0) {
+        UA_NodeReferenceKind *rk = &node->references[cp->nk];
 
         /* Reference in the right direction? */
-        if(rk->isInverse && descr->browseDirection == UA_BROWSEDIRECTION_FORWARD)
+        if(rk->isInverse && cp->bd.browseDirection == UA_BROWSEDIRECTION_FORWARD)
             continue;
-        if(!rk->isInverse && descr->browseDirection == UA_BROWSEDIRECTION_INVERSE)
+        if(!rk->isInverse && cp->bd.browseDirection == UA_BROWSEDIRECTION_INVERSE)
             continue;
 
         /* Is the reference part of the hierarchy of references we look for? */
-        if(!browseAll && !relevantReference(server, descr->includeSubtypes,
-                                            &descr->referenceTypeId, &rk->referenceTypeId))
+        UA_Boolean relevant = (referenceTypes == NULL);
+        for(size_t i = 0; i < referenceTypesSize && !relevant; i++)
+            relevant = UA_NodeId_equal(&rk->referenceTypeId, &referenceTypes[i]);
+        if(!relevant)
             continue;
 
-        /* Loop over the targets */
-        for(; targetIndex < rk->targetIdsSize; ++targetIndex) {
-            /* Get the node */
-            const UA_Node *target =
-                UA_Nodestore_getNode(server->nsCtx, &rk->targetIds[targetIndex].nodeId);
-            if(!target)
-                continue;
-
-            /* Test if the node class matches */
-            if(!matchClassMask(target, descr->nodeClassMask)) {
-                UA_Nodestore_releaseNode(server->nsCtx, target);
-                continue;
-            }
-
-            /* A match! Can we return it? */
-            if(result->referencesSize >= maxrefs) {
-                /* There are references we could not return */
-                cp->referenceKindIndex = referenceKindIndex;
-                cp->targetIndex = targetIndex;
-                UA_Nodestore_releaseNode(server->nsCtx, target);
-                return false;
-            }
-
-            /* Make enough space in the array */
-            if(result->referencesSize >= refs_size) {
-                refs_size *= 2;
-                if(refs_size > maxrefs)
-                    refs_size = maxrefs;
-                UA_ReferenceDescription *rd = (UA_ReferenceDescription*)
-                    UA_realloc(result->references, sizeof(UA_ReferenceDescription) * refs_size);
-                if(!rd) {
-                    result->statusCode = UA_STATUSCODE_BADOUTOFMEMORY;
-                    UA_Nodestore_releaseNode(server->nsCtx, target);
-                    goto error_recovery;
-                }
-                result->references = rd;
-            }
-
-            /* Copy the node description. Target is on top of the stack */
-            result->statusCode =
-                fillReferenceDescription(server, target, rk, descr->resultMask,
-                                         &result->references[result->referencesSize]);
-
-            UA_Nodestore_releaseNode(server->nsCtx, target);
-
-            if(result->statusCode != UA_STATUSCODE_GOOD)
-                goto error_recovery;
-
-            /* Increase the counter */
-            result->referencesSize++;
+        /* cp->nki is increaed in browseNodeRefKind upon success */
+        for(; cp->nki < rk->targetIdsSize; ) {
+            UA_StatusCode retval = browseNodeRefKind(server, session, cp, rr, maxed,
+                                                     rk, &rk->targetIds[cp->nki]);
+            if(*maxed || retval != UA_STATUSCODE_GOOD)
+                return retval;
         }
-
-        targetIndex = 0; /* Start at index 0 for the next reference kind */
     }
 
-    /* No relevant references, return array of length zero */
-    if(result->referencesSize == 0) {
-        UA_free(result->references);
-        result->references = (UA_ReferenceDescription*)UA_EMPTY_ARRAY_SENTINEL;
-    }
-
-    /* The node is done */
-    return true;
-
- error_recovery:
-    if(result->referencesSize == 0)
-        UA_free(result->references);
-    else
-        UA_Array_delete(result->references, result->referencesSize,
-                        &UA_TYPES[UA_TYPES_REFERENCEDESCRIPTION]);
-    result->references = NULL;
-    result->referencesSize = 0;
-    return false;
+    return UA_STATUSCODE_GOOD;
 }
 
 /* Results for a single browsedescription. This is the inner loop for both
  * Browse and BrowseNext. The ContinuationPoint contains all the data used.
  * Including the BrowseDescription. Returns whether there are remaining
  * references. */
-static UA_Boolean
-browseWithContinuation(UA_Server *server, UA_Session *session,
-                       ContinuationPoint *cp, UA_BrowseResult *result) {
-    const UA_BrowseDescription *descr = &cp->browseDescription;
-
+/* Results for a single browsedescription. Sets all NodeIds for the RefTree. */
+static UA_StatusCode
+browseWithCp(UA_Server *server, UA_Session *session, ContinuationPoint *cp,
+             RefResult *rr, UA_Boolean *maxed) {
     /* Is the browsedirection valid? */
-    if(descr->browseDirection != UA_BROWSEDIRECTION_BOTH &&
-       descr->browseDirection != UA_BROWSEDIRECTION_FORWARD &&
-       descr->browseDirection != UA_BROWSEDIRECTION_INVERSE) {
-        result->statusCode = UA_STATUSCODE_BADBROWSEDIRECTIONINVALID;
-        return true;
-    }
-
-    /* Is the reference type valid? */
-    if(!UA_NodeId_isNull(&descr->referenceTypeId)) {
-        const UA_Node *reftype = UA_Nodestore_getNode(server->nsCtx, &descr->referenceTypeId);
-        if(!reftype) {
-            result->statusCode = UA_STATUSCODE_BADREFERENCETYPEIDINVALID;
-            return true;
-        }
+    if(cp->bd.browseDirection != UA_BROWSEDIRECTION_BOTH &&
+       cp->bd.browseDirection != UA_BROWSEDIRECTION_FORWARD &&
+       cp->bd.browseDirection != UA_BROWSEDIRECTION_INVERSE) {
+        return UA_STATUSCODE_BADBROWSEDIRECTIONINVALID;
+    }
+
+    /* Set the list of references to check */
+    UA_StatusCode retval = UA_STATUSCODE_GOOD;
+    UA_NodeId *referenceTypes = NULL; /* NULL -> all reference types are relevant */
+    size_t referenceTypesSize = 0;
+    if(!UA_NodeId_isNull(&cp->bd.referenceTypeId)) {
+        const UA_Node *reftype = UA_Nodestore_getNode(server->nsCtx, &cp->bd.referenceTypeId);
+        if(!reftype)
+            return UA_STATUSCODE_BADREFERENCETYPEIDINVALID;
 
         UA_Boolean isRef = (reftype->nodeClass == UA_NODECLASS_REFERENCETYPE);
         UA_Nodestore_releaseNode(server->nsCtx, reftype);
 
-        if(!isRef) {
-            result->statusCode = UA_STATUSCODE_BADREFERENCETYPEIDINVALID;
-            return true;
+        if(!isRef)
+            return UA_STATUSCODE_BADREFERENCETYPEIDINVALID;
+
+        if(!UA_NodeId_isNull(&cp->bd.referenceTypeId)) {
+            if(cp->bd.includeSubtypes) {
+                retval = getTypesHierarchy(server->nsCtx, &cp->bd.referenceTypeId, 1,
+                                           &referenceTypes, &referenceTypesSize, true);
+                if(retval != UA_STATUSCODE_GOOD)
+                    return retval;
+                UA_assert(referenceTypesSize > 0); /* The original referenceTypeId has been mirrored back */
+            } else {
+                referenceTypes = &cp->bd.referenceTypeId;
+                referenceTypesSize = 1;
+            }
         }
     }
 
-    const UA_Node *node = UA_Nodestore_getNode(server->nsCtx, &descr->nodeId);
-    if(!node) {
-        result->statusCode = UA_STATUSCODE_BADNODEIDUNKNOWN;
-        return true;
+    for(; cp->n < cp->rt.size; cp->n++, cp->nk = 0, cp->nki = 0) {
+        /* Get the node */
+        const UA_Node *node = UA_Nodestore_getNode(server->nsCtx, &cp->rt.targets[cp->n]);
+        if(!node)
+            continue;
+
+        /* Browse the references in the current node */
+        retval = browseNode(server, session, cp, rr, maxed, referenceTypesSize, referenceTypes, node);
+        UA_Nodestore_releaseNode(server->nsCtx, node);
+        if(retval != UA_STATUSCODE_GOOD)
+            break;
+        if(!cp->recursive || *maxed)
+            break;
     }
 
-    /* Browse the references */
-    UA_Boolean done = browseReferences(server, node, cp, result);
-    UA_Nodestore_releaseNode(server->nsCtx, node);
-    return done;
+    if(referenceTypes != &cp->bd.referenceTypeId)
+        UA_Array_delete(referenceTypes, referenceTypesSize, &UA_TYPES[UA_TYPES_NODEID]);
+
+    return retval;
 }
 
 /* Start to browse with no previous cp */
 void
-Operation_Browse(UA_Server *server, UA_Session *session, const UA_UInt32 *maxrefs,
+Operation_Browse(UA_Server *server, UA_Session *session, const struct BrowseOpts *bo,
                  const UA_BrowseDescription *descr, UA_BrowseResult *result) {
-    /* Stack-allocate a temporary cp */
-    UA_STACKARRAY(ContinuationPoint, cp, 1);
-    memset(cp, 0, sizeof(ContinuationPoint));
-    cp->maxReferences = *maxrefs;
-    cp->browseDescription = *descr; /* Shallow copy. Deep-copy later if we persist the cp. */
+    /* How many references can we return at most? */
+    UA_UInt32 maxRefs = bo->maxReferences;
+    if(maxRefs == 0) {
+        if(server->config.maxReferencesPerNode != 0) {
+            maxRefs = server->config.maxReferencesPerNode;
+        } else {
+            maxRefs = UA_INT32_MAX;
+        }
+    } else {
+        if(server->config.maxReferencesPerNode != 0 &&
+           maxRefs > server->config.maxReferencesPerNode) {
+            maxRefs = server->config.maxReferencesPerNode;
+        }
+    }
 
-    UA_Boolean done = browseWithContinuation(server, session, cp, result);
+    /* Create the results array */
+    RefResult rr;
+    result->statusCode = RefResult_init(&rr, maxRefs);
+    if(result->statusCode != UA_STATUSCODE_GOOD)
+        return;
 
-    /* Exit early if done or an error occurred */
-    if(done || result->statusCode != UA_STATUSCODE_GOOD)
+    ContinuationPoint cp;
+    ContinuationPoint_init(&cp, maxRefs, bo->recursive);
+    cp.bd = *descr; /* Deep-copy only when the cp is persisted in the session */
+
+    /* Add the initial node to the RefTree */
+    result->statusCode = RefTree_add(&cp.rt, &descr->nodeId);
+    if(result->statusCode != UA_STATUSCODE_GOOD) {
+        RefTree_clear(&cp.rt);
+        RefResult_clear(&rr);
+        return;
+    }
+    
+    /* Recurse to get all references */
+    UA_Boolean maxed = false;
+    result->statusCode = browseWithCp(server, session, &cp, &rr, &maxed);
+    if(result->statusCode != UA_STATUSCODE_GOOD) {
+        RefTree_clear(&cp.rt);
+        RefResult_clear(&rr);
         return;
+    }
 
-    /* Persist the new continuation point */
+    /* No results */
+    if(rr.size == 0) {
+        result->references = (UA_ReferenceDescription*)UA_EMPTY_ARRAY_SENTINEL;
+        RefTree_clear(&cp.rt);
+        UA_free(rr.descr);
+        return;
+    }
 
-    ContinuationPoint *cp2 = NULL;
-    UA_Guid *ident = NULL;
-    UA_StatusCode retval = UA_STATUSCODE_GOOD;
+    /* Move results to the output */
+    result->references = rr.descr;
+    result->referencesSize = rr.size;
 
-    /* Enough space for the continuation point? */
-    if(session->availableContinuationPoints <= 0) {
-        retval = UA_STATUSCODE_BADNOCONTINUATIONPOINTS;
-        goto cleanup;
+    /* Nothing left for BrowseNext */
+    if(!maxed) {
+        RefTree_clear(&cp.rt);
+        return;
     }
 
-    /* Allocate and fill the data structure */
-    cp2 = (ContinuationPoint*)UA_malloc(sizeof(ContinuationPoint));
-    if(!cp2) {
+    /* Create a new continuation point. */
+    ContinuationPoint *newCp = (ContinuationPoint*)UA_malloc(sizeof(ContinuationPoint));
+    UA_StatusCode retval = UA_STATUSCODE_GOOD;
+    UA_ByteString tmp;
+    if(!newCp) {
         retval = UA_STATUSCODE_BADOUTOFMEMORY;
         goto cleanup;
     }
-    memset(cp2, 0, sizeof(ContinuationPoint));
-    cp2->referenceKindIndex = cp->referenceKindIndex;
-    cp2->targetIndex = cp->targetIndex;
-    cp2->maxReferences = cp->maxReferences;
-    retval = UA_BrowseDescription_copy(descr, &cp2->browseDescription);
-    if(retval != UA_STATUSCODE_GOOD)
-        goto cleanup;
+    *newCp = cp;
 
-    /* Create a random bytestring via a Guid */
-    ident = UA_Guid_new();
-    if(!ident) {
-        retval = UA_STATUSCODE_BADOUTOFMEMORY;
+    /* Make a deep copy of the BrowseDescription */
+    retval = UA_BrowseDescription_copy(descr, &newCp->bd);
+    if(retval != UA_STATUSCODE_GOOD)
         goto cleanup;
-    }
-    *ident = UA_Guid_random();
-    cp2->identifier.data = (UA_Byte*)ident;
-    cp2->identifier.length = sizeof(UA_Guid);
 
     /* Return the cp identifier */
-    retval = UA_ByteString_copy(&cp2->identifier, &result->continuationPoint);
+    tmp.length = sizeof(UA_Guid);
+    tmp.data = (UA_Byte*)&newCp->identifier;
+    retval = UA_ByteString_copy(&tmp, &result->continuationPoint);
     if(retval != UA_STATUSCODE_GOOD)
         goto cleanup;
 
+    /* Remove the oldest continuation point if required */
+    if(session->availableContinuationPoints <= 0) {
+        struct ContinuationPoint **prev = &session->continuationPoints;
+        struct ContinuationPoint *cp2 = session->continuationPoints;
+        while(cp2 && cp2->next) {
+            prev = &cp2->next;
+            cp2 = cp2->next;
+        }
+        if(cp2) {
+            *prev = NULL;
+            ContinuationPoint_clear(cp2);
+            UA_free(cp2);
+            ++session->availableContinuationPoints;
+        }
+    }
+
     /* Attach the cp to the session */
-    cp2->next = session->continuationPoints;
-    session->continuationPoints = cp2;
+    newCp->next = session->continuationPoints;
+    session->continuationPoints = newCp;
     --session->availableContinuationPoints;
     return;
 
  cleanup:
-    if(cp2) {
-        UA_ByteString_deleteMembers(&cp2->identifier);
-        UA_BrowseDescription_deleteMembers(&cp2->browseDescription);
-        UA_free(cp2);
+    UA_BrowseResult_deleteMembers(result); /* Holds the content that was in rr before */
+    if(newCp) {
+        ContinuationPoint_clear(newCp);
+        UA_free(newCp);
     }
-    UA_BrowseResult_deleteMembers(result);
     result->statusCode = retval;
 }
 
@@ -358,10 +581,11 @@ void Service_Browse(UA_Server *server, UA_Session *session,
         return;
     }
 
-    UA_UInt32 requestedMaxReferencesPerNode = request->requestedMaxReferencesPerNode;
+    struct BrowseOpts bo;
+    bo.maxReferences = request->requestedMaxReferencesPerNode;
+    bo.recursive = false;
     response->responseHeader.serviceResult =
-        UA_Server_processServiceOperations(server, session, (UA_ServiceOperation)Operation_Browse,
-                                           &requestedMaxReferencesPerNode,
+        UA_Server_processServiceOperations(server, session, (UA_ServiceOperation)Operation_Browse, &bo,
                                            &request->nodesToBrowseSize, &UA_TYPES[UA_TYPES_BROWSEDESCRIPTION],
                                            &response->resultsSize, &UA_TYPES[UA_TYPES_BROWSERESULT]);
 }
@@ -371,7 +595,10 @@ UA_Server_browse(UA_Server *server, UA_UInt32 maxReferences,
                  const UA_BrowseDescription *bd) {
     UA_BrowseResult result;
     UA_BrowseResult_init(&result);
-    Operation_Browse(server, &server->adminSession, &maxReferences, bd, &result);
+    struct BrowseOpts bo;
+    bo.maxReferences = maxReferences;
+    bo.recursive = false;
+    Operation_Browse(server, &server->adminSession, &bo, bd, &result);
     return result;
 }
 
@@ -381,8 +608,11 @@ Operation_BrowseNext(UA_Server *server, UA_Session *session,
                      const UA_ByteString *continuationPoint, UA_BrowseResult *result) {
     /* Find the continuation point */
     ContinuationPoint **prev = &session->continuationPoints, *cp;
+    UA_ByteString identifier;
     while((cp = *prev)) {
-        if(UA_ByteString_equal(&cp->identifier, continuationPoint))
+        identifier.length = sizeof(UA_Guid);
+        identifier.data = (UA_Byte*)&cp->identifier;
+        if(UA_ByteString_equal(&identifier, continuationPoint))
             break;
         prev = &cp->next;
     }
@@ -399,22 +629,47 @@ Operation_BrowseNext(UA_Server *server, UA_Session *session,
         return;
     }
 
-    /* Continue browsing */
-    UA_Boolean done = browseWithContinuation(server, session, cp, result);
+    /* Allocate the results array */
+    RefResult rr;
+    result->statusCode = RefResult_init(&rr, cp->maxReferences);
+    if(result->statusCode != UA_STATUSCODE_GOOD)
+        return;
+    
+    /* Recurse to get all references */
+    UA_Boolean maxed = false;
+    result->statusCode = browseWithCp(server, session, cp, &rr, &maxed);
+    if(result->statusCode != UA_STATUSCODE_GOOD)
+        goto removeCp;
+
+    /* No results */
+    if(rr.size == 0) {
+        result->references = (UA_ReferenceDescription*)UA_EMPTY_ARRAY_SENTINEL;
+        goto removeCp;
+    }
 
-    if(done) {
-        /* Remove the cp if there are no references left */
+    if(maxed) {
+        /* Keep the cp */
+        result->statusCode = UA_ByteString_copy(&identifier, &result->continuationPoint);
+        if(result->statusCode != UA_STATUSCODE_GOOD)
+            goto removeCp;
+    } else {
+        /* All done, remove the cp */
         *prev = ContinuationPoint_clear(cp);
         UA_free(cp);
         ++session->availableContinuationPoints;
-    } else {
-        /* Return the cp identifier */
-        UA_StatusCode retval = UA_ByteString_copy(&cp->identifier, &result->continuationPoint);
-        if(retval != UA_STATUSCODE_GOOD) {
-            UA_BrowseResult_deleteMembers(result);
-            result->statusCode = retval;
-        }
     }
+
+    /* Move results to the output */
+    result->references = rr.descr;
+    result->referencesSize = rr.size;
+    return;
+
+ removeCp:
+    *prev = cp->next;
+    ContinuationPoint_clear(cp);
+    UA_free(cp);
+    session->availableContinuationPoints++;
+    RefResult_clear(&rr);
 }
 
 void