Marcos Vainer Loeff

Optimizing web applications using PI Web API Batch

Blog Post created by Marcos Vainer Loeff Employee on May 9, 2016

Introduction

 

In this blog post, we will change the JavaScript application logic in order to make PI Web API Batch requests instead of using jQuery promise chains. The web app we are going to use was created using the source files from Cordova application developed for the “Develop Cross-Platform mobile apps using PI Web API” TechCon 2016 lab. As we are developing a web app and not a native app anymore, all Cordova plugins were removed from the project.

With the PI Web API 2016 release, the BATCH is now part of the PI Web API Core, which means that it is not in CTP anymore.

 

What is PI Web API Batch?

 

PI Web API Batch allows you to execute a batch of HTTP requests with just a single request by making a POST against \piwebapi\batch. The content of the request is an array with multiple objects where each one represents an HTTP request.

Those internal HTTP requests could be totally independent or not. It is possible to get the response of the first request and use it as an input of the second request as we are going to see on the following sections.

 

Please refer to the PI Web API Programming Reference for more information about BATCH.

 

How does the application work?

 

This application shows a Google Street View display. The image that the current user is seeing could be saved on the PI System by providing the following live information:

  • Latitude
  • Longitude
  • Heading
  • Pitch
  • Zoom
  • Timestamp

 

The web application sends updated values to a PI System on the cloud through a public PI Web API endpoint. This will only occur if the user provides a username. For each new user, a new element is created on the root of the AF database with 6 attributes and also 6 PI Points are created on the PI Data Archive.

 

When the application starts, those are the mainly HTTP requests that take place:

  • Get the AF database WebId
  • Get all root elements (usernames)
  • Check if the user already exists on the database
  • If it does exist, then it:
    • Creates a new event frame
    • Gets all PI Points WebIds
  • If it does not exist, then it:
    • Creates a new element on the AF database root
    • Create 6 PI Points on the PI Data Archive
    • Creates a new event frame
    • Gets all PI Points WebIds

 

The changed of the web app could be downloaded from our GitHub repository which includes both Visual Studio projects, the old one with jQuery promise chains and the new one using PI Web API Batch.

 

Let's compare the Start method from both projects:

 

            getDatabaseWebId().then(getRootElements).then(function (usersDataResponse) {
                if (usersDataResponse == null) {
                    return null;
                }
                var foundUser = false;
                usersData = usersDataResponse.Items;
                for (var i = 0; i < usersData.length; i++) {
                    if (usersData[i].Name.toLowerCase() == currentUserName.toLowerCase()) {
                        foundUser = true;
                    }
                }
                console.log('FoundUser: ' + foundUser);
                if (foundUser == false) {
                    //Exercise 4: Writing promise chains 
                    return createNewElement().then(getDataArchiveWebId).then(createUserPoints).done(function (r1, r2, r3, r4, r5, r6) {
                        showMessage('User was created!');
                        return getRootElements(null).then(getUserAttributes).then(saveUserAttributes).then(createNewEventFrame);
                    });
                }
                else {
                    showMessage('User was found!');
                    return getRootElements(null).then(getUserAttributes).then(saveUserAttributes).then(createNewEventFrame);
                }
            });

Start method using jQuery promises chains

 

     getDatabaseWebIdAndElementsRoot().then(function (data) {
                if (data[2].Status != 200) {
                    return null;
                }
                afDatabaseWebId = data[1].Content.WebId;
                var foundUser = false;
                usersData = data[2].Content.Items;
                for (var i = 0; i < usersData.length; i++) {
                    if (usersData[i].Name.toLowerCase() == currentUserName.toLowerCase()) {
                        foundUser = true;
                    }
                }
                console.log('FoundUser: ' + foundUser);
                if (foundUser == false) {
                    showMessage('User was created!');
                    return continueUserNotFound().then(saveUserAttributes);
                }
                else {
                    showMessage('User was found!');
                    return continueUserFound().then(saveUserAttributes);
                }
            });

Start method using PI Web API Batch

 

There are mainly three parts of the Start function which was updated:

 

  •   getDatabaseWebIdAndElementsRoot() replaces  getDatabaseWebId().then(getRootElements).
  •   continueUserNotFound() replaces all the jQuery promise chains in case the user is not found.
  •   continueUserFound() replaces all the jQuery promise chains in case the user is found.

 

 

Change 1 - Getting the AF Database WebId and Root Elements with BATCH

 

The functions getDatabaseWebId and getRootElements of the old project, which call processJsonContent internally, are defined below:

 


    function processJsonContent(type, data, url) {
        return $.ajax({
            type: type,
            headers: {
                "Content-Type": "application/json"
            },
            url: url,
            cache: false,
            data: data,
            async: true,
            username: 'pilabuser',
            password: 'PIWebAPI2015',
            crossDomain: true,
            xhrFields: {
                withCredentials: true
            }
        });
    }


    var getDatabaseWebId = function (data, textStatus, jqXHR) {
        console.log('PI Web API: Getting AF Database WebId....');
        var url = base_service_url + "assetdatabases?path=\\\\" + afServerName + "\\" + afDatabaseName;
        return processJsonContent('GET', null, url);
    }


    var getRootElements = function (data, textStatus, jqXHR) {
        console.log('PI Web API: Getting all users data....');
        if (data != null) {
            afDatabaseWebId = data.WebId;
        }
        var url = base_service_url + "assetdatabases/" + afDatabaseWebId + "/elements";
        return processJsonContent('GET', null, url);
    }

 

 

The new project is changed to:

 

    function processBatchRequest(batchData) {
        return $.ajax({
            type: 'POST',
            headers: {
                "Content-Type": "application/json"
            },
            url: base_service_url + 'batch',
            cache: false,
            data: JSON.stringify(batchData),
            async: true,
            username: 'pilabuser',
            password: 'PIWebAPI2015',
            crossDomain: true,
            xhrFields: {
                withCredentials: true
            }
        });
    }


    var getDatabaseWebId = function () {
        console.log('PI Web API: Getting AF Database WebId....');
        return {
            "Method": "GET",
            "Resource": base_service_url + "assetdatabases?path=\\\\" + afServerName + "\\" + afDatabaseName,
            "Headers": {
                "Cache-Control": "no-cache"
            }
        };
    }






    var getRootElements = function (parentId, parameter) {
        console.log('PI Web API: Getting PI Data Archive WebId....');
        return {
            "Method": "GET",
            "Resource": base_service_url + "assetdatabases/{0}/elements",
            "Parameters": parameter,
            "ParentIds": parentId,
        };
    }




    var getDatabaseWebIdAndElementsRoot = function () {
        var batchData = {};
        batchData["1"] = getDatabaseWebId();
        batchData["2"] = getRootElements(["1"], ["$.1.Content.WebId"]);
        return processBatchRequest(batchData);
    }











 

 

The new function called getDatabaseWebIdAndElementRoot creates an array of two objects each one representing an HTTP request. The first one gets an object from getDatabaseWebId and the second one get another object from getRootElements. This array is sent to the new method processBatchRequest which makes a POST HTTP request against /batch. Note that the getRootElements method has two inputs: parentId and parameter. Note that in order to get the root elements of the AF database, its WebId is needed. The response of the first request is needed as an input of the second one. Those inputs are to apply this behaviour.

 

Change 2 - Case when the user is found

 

Let's take a look first of the promise chains in case the user is found:

 

                else {
                    showMessage('User was found!');
                    return getRootElements(null).then(getUserAttributes).then(saveUserAttributes).then(createNewEventFrame);
                }

 

The createNewEventFrame, getUserAttributes and saveUserAttributes functions of the old project are defined below:

 




    var createNewEventFrame = function () {
        console.log('PI Web API: Creating a new AF Event Frame....');
        var url = base_service_url + "assetdatabases/" + afDatabaseWebId + "/eventframes";
        var data = new Object();
        var today = new Date();
        currentEventFrameName = currentUserName + " - " + today.toString().substring(0, 24);        
        data.Name = currentEventFrameName;
        data.Description = "Event Frame from user " + currentUserName;
        data.TemplateName = "GoogleStreetViewActivity";
        data.StartTime = "*";
        data.EndTime = "*";
        var jsonString = JSON.stringify(data);
        logged = true;
        return processJsonContent('POST', jsonString, url);
    }



  var getUserAttributes = function (data, textStatus, jqXHR) {
        console.log('PI Web API: Retrieving user attributes....');
        if (data == null) {
            return null;
        }
        usersData = data.Items;
        var url = null;
        for (var i = 0; i < usersData.length; i++) {
            if (usersData[i].Name.toLowerCase() == currentUserName.toLowerCase()) {
                url = usersData[i].Links.Attributes;
            }
        }


        return processJsonContent('GET', null, url);
    }


    var saveUserAttributes = function (data, textStatus, jqXHR) {
        userAttributesWebIds = {};
        for (var i = 0; i < data.Items.length; i++) {
            userAttributesWebIds[data.Items[i].Name] = data.Items[i].WebId;
        }
        console.log('PI Web API: Saving user attributes....');
    }


 

The new project is changed to:

 

    
    var createNewEventFrame = function () {
        console.log('PI Web API: Creating a new AF Event Frame....');
        var data = new Object();
        var today = new Date();
        currentEventFrameName = currentUserName + " - " + today.toString().substring(0, 24);
        data.Name = currentEventFrameName;
        data.Description = "Event Frame from user " + currentUserName;
        data.TemplateName = "GoogleStreetViewActivity";
        data.StartTime = "*";
        data.EndTime = "*";
        logged = true;
        return {
            "Method": "POST",
            "Resource": base_service_url + "assetdatabases/" + afDatabaseWebId + "/eventframes",
            "Content": JSON.stringify(data),
        };
    }




    var getUserElement = function (parentId) {
        console.log('PI Web API: Retrieving current element name....');
        return {
            "Method": "GET",
            "Resource": base_service_url + "elements?path=\\\\" + afServerName + "\\" + afDatabaseName + "\\" + currentUserName,
            "ParentIds": parentId
        };
    }


    var getUserAttributes = function (resource, parentIds) {
        console.log('PI Web API: Retrieving user attributes....');
        return {
            "Method": "GET",
            "Resource": resource,
            "ParentIds": parentIds
        };
    }


  var continueUserFound = function () {
        var batchData = {};
        batchData["2"] = getUserElement([]);
        batchData["1"] = getUserAttributes("$.2.Content.Links.Attributes", ["2"]);
        batchData["3"] = createNewEventFrame();
        return processBatchRequest(batchData);
    }

 

The logic of code migration is very similar of the previous change. The continueUsersFound method call three methods to get objects used to make the BATCH request. In order to make the request from getUserAttributes(), the response from the getUserElement() is needed.

 

 

Change 3 - Case when the user is NOT found

 

Let's take a look first of the promise chains in case the user is not found:

 

                    return createNewElement().then(getDataArchiveWebId).then(createUserPoints).done(function (r1, r2, r3, r4, r5, r6) {
                        showMessage('User was created!');
                        return getRootElements(null).then(getUserAttributes).then(saveUserAttributes).then(createNewEventFrame);
                    });

 

The createNewElement, getDataArchiveWebId, createUsersPoints functions of the old project are defined below:

 

 








var createNewElement = function () {        

        console.log('PI Web API: Creating a new user element....');
        //Exercise 2: Creating new elements
        var url = base_service_url + "assetdatabases/" + afDatabaseWebId + "/elements";
        var data = new Object();
        data.Name = currentUserName;
        data.Description = "Participant of the hands-on-lab";
        data.TemplateName = "UserTemplate";
        var jsonString = JSON.stringify(data);
        return processJsonContent('POST', jsonString, url);
    }



    var getDataArchiveWebId = function (data, textStatus, jqXHR) {
        console.log('PI Web API: Getting PI Data Archive WebId....');
        var url = base_service_url + "dataservers?path=\\\\" + piDataArchiveName;
        return processJsonContent('GET', null, url);


    }


    var createUserPoints = function (data, textStatus, jqXHR) {
        console.log('PI Web API: Creating user PI Points....');
        //Exercise 3: create user PI Points
        var createUserPoint = function (attributeName, pointType) {
            var url = base_service_url + "dataservers/" + piDataArchiveWebId + "/points";
            var data = new Object();
            data.Name = "CrossPlatformLab" + "." + currentUserName + "." + attributeName;
            data.PointClass = "classic";
            data.PointType = pointType;
            data.Future = false;
            var jsonString = JSON.stringify(data);
            return processJsonContent('POST', jsonString, url);
        }
        piDataArchiveWebId = data.WebId;
        var pt1 = createUserPoint('Heading', 'Float64');
        var pt2 = createUserPoint("Latitude", 'Float64');
        var pt3 = createUserPoint("Longitude", 'Float64');
        var pt4 = createUserPoint("Network Connection", 'String');
        var pt5 = createUserPoint("Pitch", 'Float64');
        var pt6 = createUserPoint("Zoom", 'Float64');
        return $.when(pt1, pt2, pt3, pt4, pt5, pt6);
    }

 

The new project is changed to:

 


    var createUserPoint = function (attributeName, pointType, parentId, parameter) {
        var data = new Object();
        data.Name = "CrossPlatformLab" + "." + currentUserName + "." + attributeName;
        data.PointClass = "classic";
        data.PointType = pointType;
        data.Future = false;


        return {
            "Method": "POST",
            "Resource": base_service_url + "dataservers/{0}/points",
            "Content": JSON.stringify(data),
            "Parameters": [parameter],
            "ParentIds": [parentId],
        };
    }


    var getDataArchiveWebId = function () {
        console.log('PI Web API: Getting PI Data Archive WebId....');
        return {
            "Method": "GET",
            "Resource": base_service_url + "dataservers?path=\\\\" + piDataArchiveName,
            "Headers": {
                "Cache-Control": "no-cache"
            }
        };
    }



    var createNewElement = function () {
        console.log('PI Web API: Creating a new user element....');
        var data = new Object();
        data.Name = currentUserName;
        data.Description = "Participant of the hands-on-lab";
        data.TemplateName = "UserTemplate";
        return {
            "Method": "POST",
            "Resource": base_service_url + "assetdatabases/" + afDatabaseWebId + "/elements",
            "Content": JSON.stringify(data),
        };
    }



    var createNewEventFrame = function () {
        console.log('PI Web API: Creating a new AF Event Frame....');
        var data = new Object();
        var today = new Date();
        currentEventFrameName = currentUserName + " - " + today.toString().substring(0, 24);
        data.Name = currentEventFrameName;
        data.Description = "Event Frame from user " + currentUserName;
        data.TemplateName = "GoogleStreetViewActivity";
        data.StartTime = "*";
        data.EndTime = "*";
        logged = true;
        return {
            "Method": "POST",
            "Resource": base_service_url + "assetdatabases/" + afDatabaseWebId + "/eventframes",
            "Content": JSON.stringify(data),
        };
    }

  var continueUserNotFound = function () {
        var batchData = {};
        batchData["1"] = getUserAttributes("$.9.Content.Links.Attributes", ["9"]);
        batchData["2"] = getDataArchiveWebId();
        batchData["3"] = createUserPoint('Heading', 'Float64', "2", "$.2.Content.WebId");
        batchData["4"] = createUserPoint("Latitude", 'Float64', "2", "$.2.Content.WebId");
        batchData["5"] = createUserPoint("Longitude", 'Float64', "2", "$.2.Content.WebId");
        batchData["6"] = createUserPoint("Network Connection", 'String', "2", "$.2.Content.WebId");
        batchData["7"] = createUserPoint("Pitch", 'Float64', "2", "$.2.Content.WebId");
        batchData["8"] = createUserPoint("Zoom", 'Float64', "2", "$.2.Content.WebId");
        batchData["9"] = getUserElement(["10"]);
        batchData["10"] = createNewElement();
        batchData["11"] = createNewEventFrame();
        return processBatchRequest(batchData);
    }



 

On the old project, we have used jQuery.when function in order to create the 6 PI Points. It makes 6 HTTP requests simultaneously as each request is independent from one another. What it was done on the new project is to merge those 6 requests with the other requests in the batch array.

 

If something is not clear just step in the code using Google Developer Tools or post a question here.

 

Conclusion

 

As you could realize migrating your web app JavaScript functions in order to use PI Web API Batch is not very difficult. It is just a matter of understanding how it works, which requests are needed and which ones needs to finish for the next ones to get started. The great benefit of doing so is the better performance your app would get as BATCH enables you to make multiple requests with a single one.

 

If you have any suggestion or questions please post a comment here.

Outcomes