require(rgdal)
require(raster)
require(RStoolbox)
require(ggplot2)
require(reshape2)
require(knitr)
require(kableExtra)
imadir2 <- "./pcatest2"

1. PCA with R

Input image

b <- brick(file.path(imadir2,"mini1.tif"))
ggRGB(b,5,3,1,stretch="lin")

Run PCA with RStoolbox and extract eigenmatrix.
(read again for time benchmarking)

t0 <- Sys.time()
b <- brick(file.path(imadir2,"mini1.tif"))
bpca <- rasterPCA(b,spca=FALSE)
trpca <- Sys.time() -t0
trpca
Time difference of 5.875336 secs
eigenvR <- loadings(bpca$model)[]

PCA image (composite of PC1 to PC3)

ggRGB(bpca$map,1,2,3,stretch="lin")

2. PCA with OTB

For OTB the current (v7.1) implementation of otbcli_DimensionalityReduction implies an awkard sequence to cope with nodata: (otbPCAtest_mini1.scr)

C:\ALOBO\OTB-7.1.0-Win64\otbenv.bat
cd D:\OTBtests\OTBpca\pcatest2
otbcli_ManageNoData.bat -in ..\mini1.tif -out mini1MASK.tif uint8 -mode.buildmask.inv 1 -mode.buildmask.outv 0 
otbcli_ManageNoData.bat -in mini1.tif -out mini1Mask.tif uint8 -mode.buildmask.inv 1 -mode.buildmask.outv 0 
#without whitening
otbcli_DimensionalityReduction.bat -in mini1.tif -bv 0 -out mini1PCAotb.tif -method pca -method.pca.whiten false -outmatrix mini1PCAotbnowhit.eigmat.csv
otbcli_ManageNoData.bat -in mini1PCAotb.tif -out mini1PCAotbM.tif -mode apply -mode.apply.mask mini1Mask.tif -mode.apply.ndval 0

3. Compare eigenvectors

eigenvOTB <- t(read.csv(file.path(imadir2,"mini1PCAotbnowhit.eigmat.csv"),header=FALSE,sep=""))
eigenvOTB <- data.frame(eigenvOTB)
eigenvR <- read.csv(file.path(imadir2,"Rmini1eigenmat.csv"),stringsAsFactors = FALSE)[,-1]
colnames(eigenvR) <- colnames(eigenvOTB) <- paste0("PC",1:5)
rownames(eigenvR) <- rownames(eigenvOTB) <- paste0("Band_",1:5)

Eigenvectors from R:

kable(eigenvR)  %>%
  kable_styling(bootstrap_options = "striped", full_width = F, position = "left")
PC1 PC2 PC3 PC4 PC5
Band_1 0.0217211 0.2969163 0.2623071 0.2694614 0.8774705
Band_2 0.1326099 0.4015052 0.0335164 -0.8967462 0.1262185
Band_3 0.0331460 0.6279545 0.5775238 0.2428258 -0.4605176
Band_4 0.3865421 0.5127257 -0.7221213 0.2534016 -0.0450125
Band_5 0.9118275 -0.3056467 0.2740052 0.0077485 -0.0034370

Eigenvectors from OTB:

kable(eigenvOTB)  %>%
  kable_styling(bootstrap_options = "striped", full_width = F, position = "left")
PC1 PC2 PC3 PC4 PC5
Band_1 0.0225902 -0.2931714 -0.2664791 0.1835331 0.8993579
Band_2 0.1346418 -0.3977168 -0.0786326 -0.9037259 0.0280966
Band_3 0.0361656 -0.6199247 -0.5704303 0.3142812 -0.4361444
Band_4 0.3874017 -0.5230245 0.7251642 0.2244495 -0.0111637
Band_5 0.9110287 0.3130665 -0.2674920 0.0210914 -0.0043922

Difference: most values < 0.01, but note larger difference values in some elements of the eigenvectors

kable(abs(eigenvR) - abs(eigenvOTB)) %>%
  kable_styling(bootstrap_options = "striped", full_width = F, position = "left")
PC1 PC2 PC3 PC4 PC5
Band_1 -0.0008691 0.0037449 -0.0041720 0.0859282 -0.0218874
Band_2 -0.0020319 0.0037884 -0.0451162 -0.0069797 0.0981219
Band_3 -0.0030196 0.0080297 0.0070935 -0.0714554 0.0243732
Band_4 -0.0008596 -0.0102988 -0.0030430 0.0289522 0.0338488
Band_5 0.0007988 -0.0074198 0.0065132 -0.0133429 -0.0009552

4. Plot PC values

rversion <- bpca$map
otbversion <- brick(file.path(imadir2,"mini1PCAotbM.tif")) #from otbPCAtest_mini1.scr
names(rversion) <- names(otbversion) <- paste0("PC",1:5)

Extract random values and tidy up for ggplot in long format

set.seed(121)
pdata <- sampleRandom(stack(rversion,otbversion), size=1000, na.rm=TRUE,
                      rowcol=TRUE, xy=TRUE, sp=FALSE)
pdata1 <- pdata[,1:9]
pdata2 <- pdata[,c(1:4,10:14)]
colnames(pdata1)[5:9] <- colnames(pdata2)[5:9] <- paste0("PC",1:5)
pdata1lf <- melt(data.frame(pdata1),id.vars=1:4)
names(pdata1lf)[6] <- "Rversion"
pdata2lf <- melt(data.frame(pdata2),id.vars=1:4)
names(pdata2lf)[6] <- "OTBversion"
pdatalf <- data.frame(cbind(pdata1lf,OTBversion=pdata2lf$OTBversion))
#kable(head(pdatalf))  %>%
#  kable_styling(bootstrap_options = "striped", full_width = F, position = "left")

Note dispersion increases for higher PCs. I’ve found this is consistent across images

ggplot(data=pdatalf) +
    geom_point(aes(x=Rversion,y=OTBversion)) +
    theme(aspect.ratio = 1) +
    facet_wrap(~variable,scale="free",ncol=3)

5. Execution times

I often need a pca output in the form of a linearly stretched PCs image in uint8 and leaving 0 for nodata, thus I include this step in the benchmarking too. The stretching is done from the 2% and 98% quantiles to the 1:255 range. For R, this linear stretching can done by combining raster::clamp() and RStoolbox::RescalImage

writeRaster(rescaleImage(clamp(bpca$map,lower=q[,1], upper=q[,2]), 
            xmin=q[,1], xmax=q[,2],ymin=1, ymax=255),
            file.path(imadir2,"mini1PCARresc"),
            datatype="INT1U", NAflag=0,
            format="GTiff",overwrite=TRUE)

Stretching with the current OTB v7.1 implies fiddling again to cope with nodata:

otbcli_DynamicConvert -in mini1PCAotbM.tif type linear -mask mini1Mask.tif -out mini1PCAotbMresc.tif uint8 -outmin 1 -outmax 255
otbcli_ManageNoData.bat -in mini1PCAotbMresc.tif -out mini1PCAotbMrescM.tif -mode apply -mode.apply.mask mini1Mask.tif -mode.apply.ndval 0

5.1 Small test image

Test with mini1.tif (small test image 85 rows x 116 cols x 5 bands x 32 bit).
Runing on a very low-profile, consumer grade old laptop:

  • Lenovo X220 with Intel Core i5-2520M CPU @2.50 GHz
  • SSD hard-drive
  • 8 Gb of RAM

Values in the table are in seconds

extimes <- read.csv("TimeBenchmark_PCA.csv",header=FALSE,stringsAsFactors = FALSE)
extimes1 <- extimes[2:4,1:3]
names(extimes1) <- extimes1[1,]
rownames(extimes1) <- extimes1[,1]
kable(extimes1[-1,-1])  %>%
  kable_styling(bootstrap_options = "striped", full_width = F, position = "left")
R OTB
PCA 2.05 1.7
stretching 0.15 1.04

5.2 Large-size image

Execution times with a real image (BertMICA20190531v2.tif) (8282 rows x 11329 cols x 5 bands x 32 bit).
For images with a real-life size, an R alternative is using gdalUtilities::gdal_transform(), but the output cannot be clamped to the 1:255 range using gdalUtilities, thus the image has to read back into R for clamp(), which significantly increases the execution time:

gdal_translate(src_dataset=file.path(rasdirpca,"BertMICA20190531v2PCARfloat.tif"),
               dst_dataset=file.path(rasdirpca,"BertMICA20190531v2PCARresc.tif"),
               scale=mscal,dryrun=FALSE)
BertMICA20190531v2PCARresc <- brick(file.path(rasdirpca,"BertMICA20190531v2PCARresc.tif"))
a1 <- clamp(BertMICA20190531v2PCARresc,lower=1,upper=255, useValues=TRUE,
            format="GTiff",datatype="INT1U",NAflag=0,
            file=file.path(rasdirpca,"BertMICA20190531v2PCARrescG.tif"),
            overwrite=TRUE)

Note the advantage of OTB: despite the fact so much reading and writing in OTB severely increases execution time, the OTB run is performed in a reasonable time while waiting for R is unpractical. Values in the table are in minutes.

extimes2 <- extimes[7:10,1:4]
names(extimes2) <- extimes2[1,]
rownames(extimes2) <- extimes2[,1]
kable(extimes2[-1,-1]) %>%
  kable_styling(bootstrap_options = "striped", full_width = F, position = "left")
R gdal+R OTB
PCA 33.73 3.36
PCA with resampling 6.9
stretching 6.38 4.43 1.66
LS0tDQp0aXRsZTogIiBDb21wYXJpbmcgUENBIHJlc3VsdHMgZnJvbSBSIGFuZCBmcm9tIE9UQiINCm91dHB1dDoNCiAgaHRtbF9ub3RlYm9vazoNCiAgIGNvZGVfZm9sZGluZzogaGlkZQ0KZWRpdG9yX29wdGlvbnM6IA0KICBjaHVua19vdXRwdXRfdHlwZTogaW5saW5lDQotLS0NCg0KKiBBZ3VzdGluLkxvYm9AaWN0amEuY3NpYy5lcw0KKiAyMDIwMDUwOA0KDQpgYGB7cn0NCnJlcXVpcmUocmdkYWwpDQpyZXF1aXJlKHJhc3RlcikNCnJlcXVpcmUoUlN0b29sYm94KQ0KcmVxdWlyZShnZ3Bsb3QyKQ0KcmVxdWlyZShyZXNoYXBlMikNCnJlcXVpcmUoa25pdHIpDQpyZXF1aXJlKGthYmxlRXh0cmEpDQppbWFkaXIyIDwtICIuL3BjYXRlc3QyIg0KYGBgDQoNCiMjIDEuIFBDQSB3aXRoIFINCklucHV0IGltYWdlDQpgYGB7ciBmaWcuaGVpZ2h0PTQsIGZpZy53aWR0aD02fQ0KYiA8LSBicmljayhmaWxlLnBhdGgoaW1hZGlyMiwibWluaTEudGlmIikpDQpnZ1JHQihiLDUsMywxLHN0cmV0Y2g9ImxpbiIpDQpgYGANClJ1biBQQ0Egd2l0aCBSU3Rvb2xib3ggYW5kIGV4dHJhY3QgZWlnZW5tYXRyaXguICANCihyZWFkIGFnYWluIGZvciB0aW1lIGJlbmNobWFya2luZykNCmBgYHtyfQ0KdDAgPC0gU3lzLnRpbWUoKQ0KYiA8LSBicmljayhmaWxlLnBhdGgoaW1hZGlyMiwibWluaTEudGlmIikpDQpicGNhIDwtIHJhc3RlclBDQShiLHNwY2E9RkFMU0UpDQp0cnBjYSA8LSBTeXMudGltZSgpIC10MA0KdHJwY2ENCmVpZ2VudlIgPC0gbG9hZGluZ3MoYnBjYSRtb2RlbClbXQ0KYGBgDQpQQ0EgaW1hZ2UgKGNvbXBvc2l0ZSBvZiBQQzEgdG8gUEMzKQ0KYGBge3IgZmlnLmhlaWdodD00LCBmaWcud2lkdGg9Nn0NCmdnUkdCKGJwY2EkbWFwLDEsMiwzLHN0cmV0Y2g9ImxpbiIpDQpgYGANCg0KIyMgMi4gUENBIHdpdGggT1RCDQpGb3IgT1RCIHRoZSBjdXJyZW50ICh2Ny4xKSBpbXBsZW1lbnRhdGlvbiBvZiBvdGJjbGlfRGltZW5zaW9uYWxpdHlSZWR1Y3Rpb24gaW1wbGllcyBhbiBhd2thcmQgc2VxdWVuY2UgdG8gY29wZSB3aXRoIG5vZGF0YToNCihvdGJQQ0F0ZXN0X21pbmkxLnNjcikNCg0KYGBgDQpDOlxBTE9CT1xPVEItNy4xLjAtV2luNjRcb3RiZW52LmJhdA0KY2QgRDpcT1RCdGVzdHNcT1RCcGNhXHBjYXRlc3QyDQpvdGJjbGlfTWFuYWdlTm9EYXRhLmJhdCAtaW4gLi5cbWluaTEudGlmIC1vdXQgbWluaTFNQVNLLnRpZiB1aW50OCAtbW9kZS5idWlsZG1hc2suaW52IDEgLW1vZGUuYnVpbGRtYXNrLm91dHYgMCANCm90YmNsaV9NYW5hZ2VOb0RhdGEuYmF0IC1pbiBtaW5pMS50aWYgLW91dCBtaW5pMU1hc2sudGlmIHVpbnQ4IC1tb2RlLmJ1aWxkbWFzay5pbnYgMSAtbW9kZS5idWlsZG1hc2sub3V0diAwIA0KI3dpdGhvdXQgd2hpdGVuaW5nDQpvdGJjbGlfRGltZW5zaW9uYWxpdHlSZWR1Y3Rpb24uYmF0IC1pbiBtaW5pMS50aWYgLWJ2IDAgLW91dCBtaW5pMVBDQW90Yi50aWYgLW1ldGhvZCBwY2EgLW1ldGhvZC5wY2Eud2hpdGVuIGZhbHNlIC1vdXRtYXRyaXggbWluaTFQQ0FvdGJub3doaXQuZWlnbWF0LmNzdg0Kb3RiY2xpX01hbmFnZU5vRGF0YS5iYXQgLWluIG1pbmkxUENBb3RiLnRpZiAtb3V0IG1pbmkxUENBb3RiTS50aWYgLW1vZGUgYXBwbHkgLW1vZGUuYXBwbHkubWFzayBtaW5pMU1hc2sudGlmIC1tb2RlLmFwcGx5Lm5kdmFsIDANCmBgYA0KIyMgMy4gQ29tcGFyZSBlaWdlbnZlY3RvcnMNCmBgYHtyfQ0KZWlnZW52T1RCIDwtIHQocmVhZC5jc3YoZmlsZS5wYXRoKGltYWRpcjIsIm1pbmkxUENBb3Ribm93aGl0LmVpZ21hdC5jc3YiKSxoZWFkZXI9RkFMU0Usc2VwPSIiKSkNCmVpZ2Vudk9UQiA8LSBkYXRhLmZyYW1lKGVpZ2Vudk9UQikNCmVpZ2VudlIgPC0gcmVhZC5jc3YoZmlsZS5wYXRoKGltYWRpcjIsIlJtaW5pMWVpZ2VubWF0LmNzdiIpLHN0cmluZ3NBc0ZhY3RvcnMgPSBGQUxTRSlbLC0xXQ0KY29sbmFtZXMoZWlnZW52UikgPC0gY29sbmFtZXMoZWlnZW52T1RCKSA8LSBwYXN0ZTAoIlBDIiwxOjUpDQpyb3duYW1lcyhlaWdlbnZSKSA8LSByb3duYW1lcyhlaWdlbnZPVEIpIDwtIHBhc3RlMCgiQmFuZF8iLDE6NSkNCmBgYA0KRWlnZW52ZWN0b3JzIGZyb20gUjoNCmBgYHtyIGNsYXNzLnNvdXJjZSA9ICdmb2xkLWhpZGUnfQ0Ka2FibGUoZWlnZW52UikgICU+JQ0KICBrYWJsZV9zdHlsaW5nKGJvb3RzdHJhcF9vcHRpb25zID0gInN0cmlwZWQiLCBmdWxsX3dpZHRoID0gRiwgcG9zaXRpb24gPSAibGVmdCIpDQpgYGANCkVpZ2VudmVjdG9ycyBmcm9tIE9UQjoNCmBgYHtyIGNsYXNzLnNvdXJjZSA9ICdmb2xkLWhpZGUnfQ0Ka2FibGUoZWlnZW52T1RCKSAgJT4lDQogIGthYmxlX3N0eWxpbmcoYm9vdHN0cmFwX29wdGlvbnMgPSAic3RyaXBlZCIsIGZ1bGxfd2lkdGggPSBGLCBwb3NpdGlvbiA9ICJsZWZ0IikNCmBgYA0KRGlmZmVyZW5jZTogbW9zdCB2YWx1ZXMgPCAwLjAxLCBidXQgbm90ZSBsYXJnZXIgZGlmZmVyZW5jZSB2YWx1ZXMgaW4gc29tZSBlbGVtZW50cyBvZiB0aGUgZWlnZW52ZWN0b3JzDQpgYGB7ciBjbGFzcy5zb3VyY2UgPSAnZm9sZC1oaWRlJ30NCmthYmxlKGFicyhlaWdlbnZSKSAtIGFicyhlaWdlbnZPVEIpKSAlPiUNCiAga2FibGVfc3R5bGluZyhib290c3RyYXBfb3B0aW9ucyA9ICJzdHJpcGVkIiwgZnVsbF93aWR0aCA9IEYsIHBvc2l0aW9uID0gImxlZnQiKQ0KYGBgDQojIyA0LiBQbG90IFBDIHZhbHVlcw0KYGBge3J9DQpydmVyc2lvbiA8LSBicGNhJG1hcA0Kb3RidmVyc2lvbiA8LSBicmljayhmaWxlLnBhdGgoaW1hZGlyMiwibWluaTFQQ0FvdGJNLnRpZiIpKSAjZnJvbSBvdGJQQ0F0ZXN0X21pbmkxLnNjcg0KbmFtZXMocnZlcnNpb24pIDwtIG5hbWVzKG90YnZlcnNpb24pIDwtIHBhc3RlMCgiUEMiLDE6NSkNCmBgYA0KDQpFeHRyYWN0IHJhbmRvbSB2YWx1ZXMgYW5kIHRpZHkgdXAgZm9yIGdncGxvdCBpbiBsb25nIGZvcm1hdA0KYGBge3J9DQpzZXQuc2VlZCgxMjEpDQpwZGF0YSA8LSBzYW1wbGVSYW5kb20oc3RhY2socnZlcnNpb24sb3RidmVyc2lvbiksIHNpemU9MTAwMCwgbmEucm09VFJVRSwNCiAgICAgICAgICAgICAgICAgICAgICByb3djb2w9VFJVRSwgeHk9VFJVRSwgc3A9RkFMU0UpDQpwZGF0YTEgPC0gcGRhdGFbLDE6OV0NCnBkYXRhMiA8LSBwZGF0YVssYygxOjQsMTA6MTQpXQ0KY29sbmFtZXMocGRhdGExKVs1OjldIDwtIGNvbG5hbWVzKHBkYXRhMilbNTo5XSA8LSBwYXN0ZTAoIlBDIiwxOjUpDQpwZGF0YTFsZiA8LSBtZWx0KGRhdGEuZnJhbWUocGRhdGExKSxpZC52YXJzPTE6NCkNCm5hbWVzKHBkYXRhMWxmKVs2XSA8LSAiUnZlcnNpb24iDQpwZGF0YTJsZiA8LSBtZWx0KGRhdGEuZnJhbWUocGRhdGEyKSxpZC52YXJzPTE6NCkNCm5hbWVzKHBkYXRhMmxmKVs2XSA8LSAiT1RCdmVyc2lvbiINCnBkYXRhbGYgPC0gZGF0YS5mcmFtZShjYmluZChwZGF0YTFsZixPVEJ2ZXJzaW9uPXBkYXRhMmxmJE9UQnZlcnNpb24pKQ0KI2thYmxlKGhlYWQocGRhdGFsZikpICAlPiUNCiMgIGthYmxlX3N0eWxpbmcoYm9vdHN0cmFwX29wdGlvbnMgPSAic3RyaXBlZCIsIGZ1bGxfd2lkdGggPSBGLCBwb3NpdGlvbiA9ICJsZWZ0IikNCmBgYA0KDQpOb3RlIGRpc3BlcnNpb24gaW5jcmVhc2VzIGZvciBoaWdoZXIgUENzLiBJJ3ZlIGZvdW5kIHRoaXMgaXMgY29uc2lzdGVudCBhY3Jvc3MgaW1hZ2VzDQoNCmBgYHtyfQ0KZ2dwbG90KGRhdGE9cGRhdGFsZikgKw0KICAgIGdlb21fcG9pbnQoYWVzKHg9UnZlcnNpb24seT1PVEJ2ZXJzaW9uKSkgKw0KICAgIHRoZW1lKGFzcGVjdC5yYXRpbyA9IDEpICsNCiAgICBmYWNldF93cmFwKH52YXJpYWJsZSxzY2FsZT0iZnJlZSIsbmNvbD0zKQ0KYGBgDQojIyA1LiBFeGVjdXRpb24gdGltZXMNCkkgb2Z0ZW4gbmVlZCBhIHBjYSBvdXRwdXQgaW4gdGhlIGZvcm0gb2YgYSBsaW5lYXJseSBzdHJldGNoZWQgUENzIGltYWdlIGluIHVpbnQ4IGFuZCBsZWF2aW5nIDAgZm9yIG5vZGF0YSwgdGh1cyBJIGluY2x1ZGUgdGhpcyBzdGVwIGluIHRoZSBiZW5jaG1hcmtpbmcgdG9vLiBUaGUgc3RyZXRjaGluZyBpcyBkb25lIGZyb20gdGhlIDIlIGFuZCA5OCUgcXVhbnRpbGVzIHRvIHRoZSAxOjI1NSByYW5nZS4NCkZvciBSLCB0aGlzIGxpbmVhciBzdHJldGNoaW5nIGNhbiBkb25lIGJ5IGNvbWJpbmluZyByYXN0ZXI6OmNsYW1wKCkgYW5kIFJTdG9vbGJveDo6UmVzY2FsSW1hZ2UNCmBgYHtyLCBldmFsID0gRkFMU0V9DQp3cml0ZVJhc3RlcihyZXNjYWxlSW1hZ2UoY2xhbXAoYnBjYSRtYXAsbG93ZXI9cVssMV0sIHVwcGVyPXFbLDJdKSwgDQogICAgICAgICAgICB4bWluPXFbLDFdLCB4bWF4PXFbLDJdLHltaW49MSwgeW1heD0yNTUpLA0KICAgICAgICAgICAgZmlsZS5wYXRoKGltYWRpcjIsIm1pbmkxUENBUnJlc2MiKSwNCiAgICAgICAgICAgIGRhdGF0eXBlPSJJTlQxVSIsIE5BZmxhZz0wLA0KICAgICAgICAgICAgZm9ybWF0PSJHVGlmZiIsb3ZlcndyaXRlPVRSVUUpDQpgYGANCg0KU3RyZXRjaGluZyB3aXRoIHRoZSBjdXJyZW50IE9UQiB2Ny4xIGltcGxpZXMgZmlkZGxpbmcgYWdhaW4gdG8gY29wZSB3aXRoIG5vZGF0YToNCmBgYA0Kb3RiY2xpX0R5bmFtaWNDb252ZXJ0IC1pbiBtaW5pMVBDQW90Yk0udGlmIHR5cGUgbGluZWFyIC1tYXNrIG1pbmkxTWFzay50aWYgLW91dCBtaW5pMVBDQW90Yk1yZXNjLnRpZiB1aW50OCAtb3V0bWluIDEgLW91dG1heCAyNTUNCm90YmNsaV9NYW5hZ2VOb0RhdGEuYmF0IC1pbiBtaW5pMVBDQW90Yk1yZXNjLnRpZiAtb3V0IG1pbmkxUENBb3RiTXJlc2NNLnRpZiAtbW9kZSBhcHBseSAtbW9kZS5hcHBseS5tYXNrIG1pbmkxTWFzay50aWYgLW1vZGUuYXBwbHkubmR2YWwgMA0KDQpgYGANCiMjIyA1LjEgU21hbGwgdGVzdCBpbWFnZQ0KVGVzdCB3aXRoIG1pbmkxLnRpZiAoc21hbGwgdGVzdCBpbWFnZSA4NSByb3dzIHggMTE2IGNvbHMgeCA1IGJhbmRzIHggMzIgYml0KS4gIA0KUnVuaW5nIG9uIGEgdmVyeSBsb3ctcHJvZmlsZSwgY29uc3VtZXIgZ3JhZGUgb2xkIGxhcHRvcDoNCg0KKiBMZW5vdm8gWDIyMCB3aXRoIEludGVsIENvcmUgaTUtMjUyME0gQ1BVIEAyLjUwIEdIeg0KKiBTU0QgaGFyZC1kcml2ZQ0KKiA4IEdiIG9mIFJBTQ0KDQpWYWx1ZXMgaW4gdGhlIHRhYmxlIGFyZSBpbiBzZWNvbmRzDQpgYGB7cn0NCmV4dGltZXMgPC0gcmVhZC5jc3YoIlRpbWVCZW5jaG1hcmtfUENBLmNzdiIsaGVhZGVyPUZBTFNFLHN0cmluZ3NBc0ZhY3RvcnMgPSBGQUxTRSkNCmV4dGltZXMxIDwtIGV4dGltZXNbMjo0LDE6M10NCm5hbWVzKGV4dGltZXMxKSA8LSBleHRpbWVzMVsxLF0NCnJvd25hbWVzKGV4dGltZXMxKSA8LSBleHRpbWVzMVssMV0NCmthYmxlKGV4dGltZXMxWy0xLC0xXSkgICU+JQ0KICBrYWJsZV9zdHlsaW5nKGJvb3RzdHJhcF9vcHRpb25zID0gInN0cmlwZWQiLCBmdWxsX3dpZHRoID0gRiwgcG9zaXRpb24gPSAibGVmdCIpDQpgYGANCiMjIyA1LjIgTGFyZ2Utc2l6ZSBpbWFnZQ0KRXhlY3V0aW9uIHRpbWVzIHdpdGggYSByZWFsIGltYWdlIChCZXJ0TUlDQTIwMTkwNTMxdjIudGlmKSAoODI4MiByb3dzIHggMTEzMjkgY29scyB4IDUgYmFuZHMgeCAzMiBiaXQpLiAgDQpGb3IgaW1hZ2VzIHdpdGggYSByZWFsLWxpZmUgc2l6ZSwgYW4gUiBhbHRlcm5hdGl2ZSBpcyB1c2luZyBnZGFsVXRpbGl0aWVzOjpnZGFsX3RyYW5zZm9ybSgpLCBidXQgdGhlIG91dHB1dCBjYW5ub3QgYmUgY2xhbXBlZCB0byB0aGUgMToyNTUgcmFuZ2UgdXNpbmcgZ2RhbFV0aWxpdGllcywgdGh1cyB0aGUgaW1hZ2UgaGFzIHRvIHJlYWQgYmFjayBpbnRvIFIgZm9yIGNsYW1wKCksIHdoaWNoIHNpZ25pZmljYW50bHkgaW5jcmVhc2VzIHRoZSBleGVjdXRpb24gdGltZToNCmBgYHtyLCBldmFsID0gRkFMU0V9DQpnZGFsX3RyYW5zbGF0ZShzcmNfZGF0YXNldD1maWxlLnBhdGgocmFzZGlycGNhLCJCZXJ0TUlDQTIwMTkwNTMxdjJQQ0FSZmxvYXQudGlmIiksDQogICAgICAgICAgICAgICBkc3RfZGF0YXNldD1maWxlLnBhdGgocmFzZGlycGNhLCJCZXJ0TUlDQTIwMTkwNTMxdjJQQ0FScmVzYy50aWYiKSwNCiAgICAgICAgICAgICAgIHNjYWxlPW1zY2FsLGRyeXJ1bj1GQUxTRSkNCkJlcnRNSUNBMjAxOTA1MzF2MlBDQVJyZXNjIDwtIGJyaWNrKGZpbGUucGF0aChyYXNkaXJwY2EsIkJlcnRNSUNBMjAxOTA1MzF2MlBDQVJyZXNjLnRpZiIpKQ0KYTEgPC0gY2xhbXAoQmVydE1JQ0EyMDE5MDUzMXYyUENBUnJlc2MsbG93ZXI9MSx1cHBlcj0yNTUsIHVzZVZhbHVlcz1UUlVFLA0KICAgICAgICAgICAgZm9ybWF0PSJHVGlmZiIsZGF0YXR5cGU9IklOVDFVIixOQWZsYWc9MCwNCiAgICAgICAgICAgIGZpbGU9ZmlsZS5wYXRoKHJhc2RpcnBjYSwiQmVydE1JQ0EyMDE5MDUzMXYyUENBUnJlc2NHLnRpZiIpLA0KICAgICAgICAgICAgb3ZlcndyaXRlPVRSVUUpDQpgYGANCg0KTm90ZSB0aGUgYWR2YW50YWdlIG9mIE9UQjogZGVzcGl0ZSB0aGUgZmFjdCBzbyBtdWNoIHJlYWRpbmcgYW5kIHdyaXRpbmcgaW4gT1RCIHNldmVyZWx5IGluY3JlYXNlcyBleGVjdXRpb24gdGltZSwgdGhlIE9UQiBydW4gaXMgcGVyZm9ybWVkIGluIGEgcmVhc29uYWJsZSB0aW1lIHdoaWxlIHdhaXRpbmcgZm9yIFIgaXMgdW5wcmFjdGljYWwuDQpWYWx1ZXMgaW4gdGhlIHRhYmxlIGFyZSBpbiBtaW51dGVzLg0KDQoNCmBgYHtyfQ0KZXh0aW1lczIgPC0gZXh0aW1lc1s3OjEwLDE6NF0NCm5hbWVzKGV4dGltZXMyKSA8LSBleHRpbWVzMlsxLF0NCnJvd25hbWVzKGV4dGltZXMyKSA8LSBleHRpbWVzMlssMV0NCmthYmxlKGV4dGltZXMyWy0xLC0xXSkgJT4lDQogIGthYmxlX3N0eWxpbmcoYm9vdHN0cmFwX29wdGlvbnMgPSAic3RyaXBlZCIsIGZ1bGxfd2lkdGggPSBGLCBwb3NpdGlvbiA9ICJsZWZ0IikNCmBgYA0K