Session 11: Mastering {data.table}: efficient operations on large datasets. Probability: Conditional Probability. The Bayes’ Theorem.

Feedback should be send to goran.milovanovic@datakolektiv.com. These notebooks accompany the Intro to Data Science: Non-Technical Background course 2020/21.


What do we want to do today?

Getting to know {data.table}, the essential package for efficient processing of large datasets in RAM in R. Comparison: {dplyr} and {data.table}: that would be the “data” part of today’s session. The “science” part considers conditional probabilities and the famous Bayes’ Theorem.

0. Prerequisits

  1. nycflights13 should be there already; also
  2. `install.packages(‘data.table’) - but I think we have already used it?
library(tidyverse)
library(data.table)
dataDir <- paste0(getwd(), "/_data/")

1. {data.table}

1.1 Efficiently read large datasets in R: data.table::fread()

We will use the flights dataset from the {nycflights13} package in this Session:

flightsFrame <- nycflights13::flights
dim(flightsFrame)

Let’s be clear: a data frame with 336776 rows and 19 columns is everything but “big” in any sense nowadays. However:

write.csv(flightsFrame, paste0(dataDir, "flights.csv"))

And now it is just a .csv file in our local filesystem.

system.time(flightsFrame_baseR <- read.csv(paste0(dataDir, "flights.csv"),
                                           header = T,
                                           row.names = 1,
                                           stringsAsFactors = F,
                                           check.names = F)
            )

With data.table::fread():

system.time(flightsFrame_DT <- fread(paste0(dataDir, "flights.csv"),
                                     header = T)
            )

Compare..?

cmp1 <- system.time(flightsFrame_baseR <- read.csv(paste0(dataDir, "flights.csv"),
                                                   header = T,
                                                   row.names = 1,
                                                   stringsAsFactors = F,
                                                   check.names = F)
                    )
cmp2 <- system.time(flightsFrame_DT <- fread(paste0(dataDir, "flights.csv"),
                                                header = T)
                    )
as.numeric(cmp1[1])/as.numeric(cmp2[1])

The result may vary for various reasons: however, data.table::fread() does it order of magnitude faster than the baseR read.csv(). There is no reason to abandon read.csv() (or the tidyverse {readr} package functions). But when it comes to really large datasets… fread().

Please be reminded of the following:

class(flightsFrame_baseR)
class(flightsFrame_DT)

There is a specific data.table class attached to R objects used as data.tables.

rm(flightsFrame_baseR)
rm(flightsFrame)

1.2 {data.table} essential operations

Subsetting data.table can be similar to what we have already learned about base R.

flightsFrame_DT$V1 <- NULL
flightsFrame_DT[3:4, ]

Exclude rows by negative indexing (taking a small dataset to illustrate)

irisDT <- data.table(iris) 
irisDT[!3:7, ]

Filter rows in {data.table}:

flightsFrame_DT[month > 6]

Let’s compare with {dplyr}:

cmp1 <- system.time(flightsFrame_DT[month > 6])
cmp2 <- system.time(filter(flightsFrame_DT, month > 6))
print("{data.table}:")
print(cmp1)
print("{dplyr}:")
print(cmp2)

Filter using multiple conditions

cmp1 <- system.time(flightsFrame_DT[month == 1 & dep_delay > 0])
cmp2 <- system.time(filter(flightsFrame_DT, month > 1 & dep_delay > 0))
print("{data.table}:")
print(cmp1)
print("{dplyr}:")
print(cmp2)

Sorting rows:

cmp1 <- system.time(flightsFrame_DT[order(dep_delay)])
cmp2 <- system.time(arrange(flightsFrame_DT, dep_delay))
print("{data.table}:")
print(cmp1)
print("{dplyr}:")
print(cmp2)

Enters data.table::setkey()

setkey(flightsFrame_DT, dep_delay)
key(flightsFrame_DT)
cmp1 <- system.time(flightsFrame_DT[order(dep_delay)])
cmp2 <- system.time(arrange(flightsFrame_DT, dep_delay))
print("{data.table}:")
print(cmp1)
print("{dplyr}:")
print(cmp2)

Please be aware of the fact that the system.time() results will vary, and that our flightsFrame_DT is far from being of a size considerable for comparisons between {data.table} and {dplyr} or base R.

Selecting columns is done in the following way. To return a data.table object from an existing data.table:

arr_time <- flightsFrame_DT[ , list(arr_time)]
class(arr_time)

The following accomplishes the same with . used as an alias for list():

arr_time <- flightsFrame_DT[, .(arr_time)]
class(arr_time)

To obtain a vector from a data.table column:

arr_time <- flightsFrame_DT[, arr_time]
class(arr_time)

Or:

arr_time <- flightsFrame_DT[['arr_time']]
class(arr_time)

Now we can try a simple conjunction of filtering and selection operations in {data.table}, for example:

selectedFlights <- 
  flightsFrame_DT[arr_time > 100, .(arr_time, arr_delay, dest)]
head(selectedFlights)

The {dplyr} equivalent is:

selectedFlights_dplyr <- flightsFrame_DT %>% 
  select(arr_time, arr_delay, dest) %>% 
  filter(arr_time > 100)
head(selectedFlights_dplyr)

Let’s create new variables:

arr_time_hours <- 
  flightsFrame_DT[air_time > 20, .(air_time, air_time_hours = air_time/60)]
head(arr_time_hours)

Enters := - the column assignment operator in {data.table}, which allows to modify a data.table object by reference (to be explained in the Session). We will first filter out all rows with an NA value in air_time:

dim(flightsFrame_DT)
flightsFrame_DT <- flightsFrame_DT[!is.na(air_time)]
dim(flightsFrame_DT)

Using := we can modify a data.table object without having to make a copy of it - which happens with both base R data.frame objects and in {dplyr}:

flightsFrame_DT[ , air_time_hours := air_time/60]
flightsFrame_DT[ , .(air_time, air_time_hours)]

1.3 {data.table} grouping (by =), aggregation, and joins

A simple aggregation:

flightsFrame_DT[ , .(avg_air_time = mean(air_time)), by = dest]

This is equivalent to {dplyr}:

flightsFrame_DT %>% 
  select(dest, air_time) %>% 
  group_by(dest) %>% 
  summarise(avg_air_time = mean(air_time))

Simple aggregation with filtering:

flightsFrame_DT[dep_delay <= 0, 
                .(avg_dep_delay = mean(dep_delay), avg_air_time = mean(air_time)), 
                by = dest]

Count rows (observations) by groups:

flightsFrame_DT[dep_delay <= 0, .N, by = dest]

To demonstrate a join operation in {data.table} load the planes data.frame from nycflights13:

planes <- nycflights13::planes
head(planes)

As a reminder, this is how it would be done in {dplyr} with left_join() over tailnum (see Session 10):

flights_relations <- dplyr::left_join(flightsFrame_DT,
                                      planes,
                                      by = "tailnum")

In {data.table}, first use setkey(), then promote planes to a data.table object, and finally use merge():

setkey(flightsFrame_DT, tailnum)
planes <- data.table(planes)
setkey(planes, tailnum)
flightPlanes <- merge(flightsFrame_DT,
                      planes,
                      by = "tailnum",
                      all.x = T)

Oh. One final thing… data.table::fwrite().

system.time(
  write.csv(flightsFrame_DT, paste0(dataDir, "flightsFrame_DT_writecsv.csv"))
  )
system.time(
  fwrite(flightsFrame_DT, paste0(dataDir, "flightsFrame_DT_fwrite.csv"))
  )

4. Conditional Probability and Bayes’ Theorem

4.1 Conditional Probability

Imagine the following situation: there are two popular social groups, A and B, and we have the knowledge on how many men and women are members of them: in group A we find 87 men and 57 women, while in group B we find 57 men and 96 women.

probs <- data.frame(Group = c('A', 'B'),
                    Male = c(87, 44), 
                    Female = c(57, 96))
probs

Now imagine that we want to make one random draw from this sample and identify one single individual. What is the probability to randomly select a man vs a woman?

Let’s see: we have

men <- sum(probs$Male)
print(men)
[1] 131

men, and

women <- sum(probs$Female)
print(women)
[1] 153

women, so the probability P(man) would be:

p_man <- men/(men + women)
p_man
[1] 0.4612676

where men + women is also the total sample size. The probability to randomly draw a woman from the sample is:

p_woman <- women/(men + women)
p_woman
[1] 0.5387324

Of course,

p_man + p_woman
[1] 1

Now: what would be the probability to randomly draw a man from the sample if we already know that we will be picking someone from group A? Let’s take a look at the sample once again:

probs

Obviously, we would focus our attention on the first row only, where we find 87 men and 57 women, neglecting the second row because we already know that the person is a member of group A.

cp_man_A <- probs$Male[probs$Group == 'A']/(probs$Male[probs$Group == 'A'] + probs$Female[probs$Group == 'A'])
cp_man_A
[1] 0.6041667
cp_woman_A <- 
  probs$Female[probs$Group == 'A']/(probs$Male[probs$Group == 'A'] + probs$Female[probs$Group == 'A'])
cp_woman_A
[1] 0.3958333

And again we have:

cp_woman_A + cp_man_A
[1] 1

The probabilities cp_man_A and cp_woman_A are called conditional probabilities and play a very important role in many mathematical models used in Data Science and Machine Learning:

\(P(Y|X) = \frac{P(Y{\cap}X)}{P(X)}\)

where \(P(Y|X)\) is the conditional probability of observing Y given that we already know that X obtains, while \({P(X{\cap}Y)}\) is the joint probability of observing both X and Y. Let’ see: what is the joint probability of observing both a person from group A and a man? From the table we see that there are 87 men in group A, so that probability must be 87 divided by the total sample size which is 284:

probs$Male[1]/sum(probs[, 2:3])
[1] 0.306338

Now we need to divide this joint probability by P(A) in total:

sum(probs[1, 2:3])/sum(probs[, 2:3])
[1] 0.5070423

and the desired conditional probability is:

jointP <- probs$Male[1]/sum(probs[, 2:3])
totalP <- sum(probs[1, 2:3])/sum(probs[, 2:3])
jointP/totalP
[1] 0.6041667

4.2 Independence

Two events, \(X\) and \(Y\), are said to be statistically independent iff:

\[P(X{\cap}Y) = P(X)P(Y)\]

If \(P(X)\) is not zero, and given that

\[P(Y|X) = \frac{P(Y{\cap}X)}{P(X)}\],

that means that statistical independence implies something very intuitive, namely:

\[P(Y|X) = P(Y)\]

4.3 Bayes’ Theorem

It can be easily shown that the following holds:

\[P(Y|X) = \frac{P(X|Y)P(Y)}{P(X)}\]

The expression is called Bayes’ Theorem and plays a role of immense importance in contemporary mathematical statistics, Data Science and Machine Learning.

This is how we speak of its terms:

\[P(Y|X)\]

is the posterior probability of obtaining \(Y\) from knowing \(X\)

\[P(X|Y)\]

is the likelihood of obtaining \(X\) from knowing \(Y\)

\[P(Y)\]

is the prior probability of obtaining \(Y\).

4.4 Bayesian Inference for a Binomial Distribution from a Beta Prior

Imagine that we wish to express our belief about the parameter \(p\) of a Binomial Distribution by another function. It can be shown that it makes sense to use the Beta Distribution to express such beliefs, which has a support constrained to [0, 1]:

\[P(x;\alpha, \beta) = \frac{x^{\alpha-1}(1-x)^{\beta-1}}{B(\alpha,\beta)}\]

where \(B(\alpha,\beta)\) is the Beta function

\[B(\alpha,\beta)=\frac{\Gamma(\alpha)\Gamma(\beta)}{\Gamma(\alpha)+\Gamma(\beta)}\]

and serves the purposes of normalization only.

For example, a \(Beta(x;\alpha,\beta)\) distribution with \(\alpha = 7\) and \(\beta=13\) looks like this:

library(ggplot2)
x <- seq(.01, .99, by = .01)
density <- dbeta(x, 7, 13)
densFrame <- data.frame(x = x, 
                        density = density)
ggplot(densFrame, aes(x = x, y = density)) + 
  geom_path(size = .15, group = 1, color = "red") + 
  ggtitle("Beta(3,7)") + 
  theme_bw() + 
  theme(panel.border = element_blank())

And this is how \(Beta(x;\alpha,\beta)\) distribution with \(\alpha = 1\) and \(\beta=1\) (i.e. the Uniform Prior) looks like

library(ggplot2)
x <- seq(.01, .99, by = .01)
density <- dbeta(x, 1, 1)
densFrame <- data.frame(x = x, 
                        density = density)
ggplot(densFrame, aes(x = x, y = density)) + 
  geom_path(size = .15, group = 1, color = "red") + 
  ggtitle("Beta(1,1)") + 
  theme_bw() + 
  theme(panel.border = element_blank())

While \(Beta(x;\alpha,\beta)\) distribution with \(\alpha = 1/2\) and \(\beta=1/2\) looks like this (this is also called a Jeffrey’s Prior):

library(ggplot2)
x <- seq(.01, .99, by = .01)
density <- dbeta(x, 1/2, 1/2)
densFrame <- data.frame(x = x, 
                        density = density)
ggplot(densFrame, aes(x = x, y = density)) + 
  geom_path(size = .15, group = 1, color = "red") + 
  ggtitle("Beta(1/2,1/2)") + 
  theme_bw() + 
  theme(panel.border = element_blank())

Turns out that the \(Beta(\alpha, \beta)\) distribution has a nice property that makes it a suitable conjugate prior distribution in Bayesian inference for the Binomial Distribution. Namely, if our prior beliefs about the Binomial \(p\) parameter are expressed as \(Beta(p; \alpha, \beta)\), and then we observe \(x\) successes from \(n\) trials in a Binomial experiment, our posterior belief about the Binomial \(p\) parameter can be expressed as:

\[Beta(p;\alpha',\beta')\] where

\[\alpha' = \alpha + x\]

and

\[\beta' = \beta + n - x\] Let’s illustrate. Say that in the beginning we know nothing about the possible value of \(p\). We express this absence of knowledge by a uniform prior, \(Beta(\alpha=1, \beta=1))\):

x <- seq(.01, .99, by = .01)
density <- dbeta(x, 1, 1)
densFrame <- data.frame(p = x, 
                        density = density)
ggplot(densFrame, aes(x = p, y = density)) + 
  geom_path(size = .35, group = 1, color = "red") + 
  ggtitle("Our a priori is: Beta(1,1)") + 
  theme_bw() + 
  theme(panel.border = element_blank())

Let’s assume that than we observe 1,000 coin tosses of which 275 resulted in success (i.e. \(Head\)); we update our prior beliefs accordingly:

library(tidyr)
x <- seq(.01, .99, by = .01)
# - prior
alpha <- 1
beta <- 1
density <- dbeta(x, alpha, beta)
# - update
post_alpha <- alpha + 275
post_beta <- beta + 1000 - 275
post_density <- dbeta(x, post_alpha, post_beta) 
densFrame <- data.frame(p = x, 
                        prior = density,
                        posterior = post_density) %>% 
  tidyr::pivot_longer(-p, 
                      names_to = "beliefs",
                      values_to = "value")
ggplot(densFrame, aes(x = p, 
                      y = value,
                      group = beliefs, 
                      color = beliefs)) + 
  geom_path(size = .35, ) + 
  ggtitle("Prior and Posterior") + 
  theme_bw() + 
  theme(panel.border = element_blank())

And what if we have some prior knowlegde and do not wish to begin from a uniform prior distribution? What if we have already observed 1,000 coin tosses that resulted in 275 heads, and then only we observe another 500 tosses resulting in 150 heads (weird, but still)?

library(tidyr)
x <- seq(.01, .99, by = .01)
# - prior
alpha <- 276
beta <- 726
density <- dbeta(x, alpha, beta)
# - update
post_alpha <- alpha + 150
post_beta <- beta + 500 - 150
post_density <- dbeta(x, post_alpha, post_beta) 
densFrame <- data.frame(p = x, 
                        prior = density,
                        posterior = post_density) %>% 
  tidyr::pivot_longer(-p, 
                      names_to = "beliefs",
                      values_to = "value")
ggplot(densFrame, aes(x = p, 
                      y = value,
                      group = beliefs, 
                      color = beliefs)) + 
  geom_path(size = .35, ) + 
  ggtitle("Prior and Posterior") + 
  theme_bw() + 
  theme(panel.border = element_blank())

R Markdown

R Markdown is what I have used to produce this beautiful Notebook. We will learn more about it near the end of the course, but if you already feel ready to dive deep, here’s a book: R Markdown: The Definitive Guide, Yihui Xie, J. J. Allaire, Garrett Grolemunds.


Goran S. Milovanović

DataKolektiv, 2020/21

contact:


License: GPLv3 This Notebook is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This Notebook is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this Notebook. If not, see http://www.gnu.org/licenses/.


LS0tCnRpdGxlOiBJbnRybyB0byBEYXRhIFNjaWVuY2UgKE5vbi1UZWNobmljYWwgQmFja2dyb3VuZCwgUikgLSBTZXNzaW9uMTEKYXV0aG9yOgotIG5hbWU6IEdvcmFuIFMuIE1pbG92YW5vdmnEhywgUGhECiAgYWZmaWxpYXRpb246IERhdGFLb2xla3RpdiwgQ2hpZWYgU2NpZW50aXN0ICYgT3duZXI7IERhdGEgU2NpZW50aXN0IGZvciBXaWtpZGF0YSwgV01ERQphYnN0cmFjdDogCm91dHB1dDoKICBodG1sX25vdGVib29rOgogICAgY29kZV9mb2xkaW5nOiBzaG93CiAgICB0aGVtZTogc3BhY2VsYWIKICAgIHRvYzogeWVzCiAgICB0b2NfZmxvYXQ6IHllcwogICAgdG9jX2RlcHRoOiA1CiAgaHRtbF9kb2N1bWVudDoKICAgIHRvYzogeWVzCiAgICB0b2NfZGVwdGg6IDUKLS0tCgohW10oLi4vX2ltZy9ES19Mb2dvXzEwMC5wbmcpCgoqKioKIyBTZXNzaW9uIDExOiBNYXN0ZXJpbmcge2RhdGEudGFibGV9OiBlZmZpY2llbnQgb3BlcmF0aW9ucyBvbiBsYXJnZSBkYXRhc2V0cy4gUHJvYmFiaWxpdHk6IENvbmRpdGlvbmFsIFByb2JhYmlsaXR5LiBUaGUgQmF5ZXPigJkgVGhlb3JlbS4KCioqRmVlZGJhY2sqKiBzaG91bGQgYmUgc2VuZCB0byBgZ29yYW4ubWlsb3Zhbm92aWNAZGF0YWtvbGVrdGl2LmNvbWAuIApUaGVzZSBub3RlYm9va3MgYWNjb21wYW55IHRoZSBJbnRybyB0byBEYXRhIFNjaWVuY2U6IE5vbi1UZWNobmljYWwgQmFja2dyb3VuZCBjb3Vyc2UgMjAyMC8yMS4KCioqKgoKIyMjIFdoYXQgZG8gd2Ugd2FudCB0byBkbyB0b2RheT8KCkdldHRpbmcgdG8ga25vdyBge2RhdGEudGFibGV9YCwgdGhlIGVzc2VudGlhbCBwYWNrYWdlIGZvciBlZmZpY2llbnQgcHJvY2Vzc2luZyBvZiBsYXJnZSBkYXRhc2V0cyBpbiBSQU0gaW4gUi4gQ29tcGFyaXNvbjoge2RwbHlyfSBhbmQge2RhdGEudGFibGV9OiB0aGF0IHdvdWxkIGJlIHRoZSAiZGF0YSIgcGFydCBvZiB0b2RheSdzIHNlc3Npb24uIFRoZSAic2NpZW5jZSIgcGFydCBjb25zaWRlcnMgY29uZGl0aW9uYWwgcHJvYmFiaWxpdGllcyBhbmQgdGhlIGZhbW91cyBCYXllcycgVGhlb3JlbS4KCgojIyMgMC4gUHJlcmVxdWlzaXRzCgogIDEuIGBueWNmbGlnaHRzMTNgIHNob3VsZCBiZSB0aGVyZSBhbHJlYWR5OyBhbHNvCiAgMi4gYGluc3RhbGwucGFja2FnZXMoJ2RhdGEudGFibGUnKSAtIGJ1dCBJIHRoaW5rIHdlIGhhdmUgYWxyZWFkeSB1c2VkIGl0PwogIApgYGB7ciBlY2hvID0gVCwgbWVzc2FnZSA9IEYsIHdhcm5pbmcgPSBGfQpsaWJyYXJ5KHRpZHl2ZXJzZSkKbGlicmFyeShkYXRhLnRhYmxlKQpkYXRhRGlyIDwtIHBhc3RlMChnZXR3ZCgpLCAiL19kYXRhLyIpCmBgYAoKIyMjIDEuIHtkYXRhLnRhYmxlfQoKIyMjIyAxLjEgRWZmaWNpZW50bHkgcmVhZCBsYXJnZSBkYXRhc2V0cyBpbiBSOiBgZGF0YS50YWJsZTo6ZnJlYWQoKWAKCldlIHdpbGwgdXNlIHRoZSBgZmxpZ2h0c2AgZGF0YXNldCBmcm9tIHRoZSBge255Y2ZsaWdodHMxM31gIHBhY2thZ2UgaW4gdGhpcyBTZXNzaW9uOiAKCmBgYHtyIGVjaG8gPSBUfQpmbGlnaHRzRnJhbWUgPC0gbnljZmxpZ2h0czEzOjpmbGlnaHRzCmRpbShmbGlnaHRzRnJhbWUpCmBgYApMZXQncyBiZSBjbGVhcjogYSBkYXRhIGZyYW1lIHdpdGggYDMzNjc3NmAgcm93cyBhbmQgYDE5YCBjb2x1bW5zIGlzIGV2ZXJ5dGhpbmcgYnV0ICJiaWciIGluIGFueSBzZW5zZSBub3dhZGF5cy4gSG93ZXZlcjoKCmBgYHtyIGVjaG8gPSBUfQp3cml0ZS5jc3YoZmxpZ2h0c0ZyYW1lLCBwYXN0ZTAoZGF0YURpciwgImZsaWdodHMuY3N2IikpCmBgYAoKQW5kIG5vdyBpdCBpcyBqdXN0IGEgYC5jc3ZgIGZpbGUgaW4gb3VyIGxvY2FsIGZpbGVzeXN0ZW0uCgpgYGB7ciBlY2hvID0gVH0Kc3lzdGVtLnRpbWUoZmxpZ2h0c0ZyYW1lX2Jhc2VSIDwtIHJlYWQuY3N2KHBhc3RlMChkYXRhRGlyLCAiZmxpZ2h0cy5jc3YiKSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGhlYWRlciA9IFQsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICByb3cubmFtZXMgPSAxLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgc3RyaW5nc0FzRmFjdG9ycyA9IEYsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBjaGVjay5uYW1lcyA9IEYpCiAgICAgICAgICAgICkKYGBgCldpdGggYGRhdGEudGFibGU6OmZyZWFkKClgOgoKYGBge3IgZWNobyA9IFR9CnN5c3RlbS50aW1lKGZsaWdodHNGcmFtZV9EVCA8LSBmcmVhZChwYXN0ZTAoZGF0YURpciwgImZsaWdodHMuY3N2IiksCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBoZWFkZXIgPSBUKQogICAgICAgICAgICApCmBgYApDb21wYXJlLi4/CgpgYGB7ciBlY2hvID0gVH0KY21wMSA8LSBzeXN0ZW0udGltZShmbGlnaHRzRnJhbWVfYmFzZVIgPC0gcmVhZC5jc3YocGFzdGUwKGRhdGFEaXIsICJmbGlnaHRzLmNzdiIpLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBoZWFkZXIgPSBULAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICByb3cubmFtZXMgPSAxLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBzdHJpbmdzQXNGYWN0b3JzID0gRiwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgY2hlY2submFtZXMgPSBGKQogICAgICAgICAgICAgICAgICAgICkKY21wMiA8LSBzeXN0ZW0udGltZShmbGlnaHRzRnJhbWVfRFQgPC0gZnJlYWQocGFzdGUwKGRhdGFEaXIsICJmbGlnaHRzLmNzdiIpLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBoZWFkZXIgPSBUKQogICAgICAgICAgICAgICAgICAgICkKYXMubnVtZXJpYyhjbXAxWzFdKS9hcy5udW1lcmljKGNtcDJbMV0pCmBgYApUaGUgcmVzdWx0IG1heSB2YXJ5IGZvciB2YXJpb3VzIHJlYXNvbnM6IGhvd2V2ZXIsIGBkYXRhLnRhYmxlOjpmcmVhZCgpYCBkb2VzIGl0IG9yZGVyIG9mIG1hZ25pdHVkZSBmYXN0ZXIgdGhhbiB0aGUgYmFzZVIgYHJlYWQuY3N2KClgLiBUaGVyZSBpcyBubyByZWFzb24gdG8gYWJhbmRvbiBgcmVhZC5jc3YoKWAgKG9yIHRoZSAqdGlkeXZlcnNlKiBge3JlYWRyfWAgcGFja2FnZSBmdW5jdGlvbnMpLiBCdXQgd2hlbiBpdCBjb21lcyB0byByZWFsbHkgbGFyZ2UgZGF0YXNldHMuLi4gYGZyZWFkKClgLiAKClBsZWFzZSBiZSByZW1pbmRlZCBvZiB0aGUgZm9sbG93aW5nOgoKYGBge3IgZWNobyA9IFR9CmNsYXNzKGZsaWdodHNGcmFtZV9iYXNlUikKY2xhc3MoZmxpZ2h0c0ZyYW1lX0RUKQpgYGAKVGhlcmUgaXMgYSBzcGVjaWZpYyBgZGF0YS50YWJsZWAgY2xhc3MgYXR0YWNoZWQgdG8gUiBvYmplY3RzIHVzZWQgYXMgYGRhdGEudGFibGVzYC4gCgpgYGB7ciBlY2hvID0gVH0Kcm0oZmxpZ2h0c0ZyYW1lX2Jhc2VSKQpybShmbGlnaHRzRnJhbWUpCmBgYAoKIyMjIyAxLjIge2RhdGEudGFibGV9IGVzc2VudGlhbCBvcGVyYXRpb25zCgpTdWJzZXR0aW5nIGRhdGEudGFibGUgY2FuIGJlIHNpbWlsYXIgdG8gd2hhdCB3ZSBoYXZlIGFscmVhZHkgbGVhcm5lZCBhYm91dCBiYXNlIFIuCgpgYGB7ciBlY2hvID0gVH0KZmxpZ2h0c0ZyYW1lX0RUJFYxIDwtIE5VTEwKZmxpZ2h0c0ZyYW1lX0RUWzM6NCwgXQpgYGAKCkV4Y2x1ZGUgcm93cyBieSBuZWdhdGl2ZSBpbmRleGluZyAodGFraW5nIGEgc21hbGwgZGF0YXNldCB0byBpbGx1c3RyYXRlKQoKYGBge3IgZWNobyA9IFR9CmlyaXNEVCA8LSBkYXRhLnRhYmxlKGlyaXMpIAppcmlzRFRbITM6NywgXQpgYGAKCkZpbHRlciByb3dzIGluIHtkYXRhLnRhYmxlfToKCmBgYHtyIGVjaG8gPSBUfQpmbGlnaHRzRnJhbWVfRFRbbW9udGggPiA2XQpgYGAKTGV0J3MgY29tcGFyZSB3aXRoIHtkcGx5cn06CgpgYGB7ciBlY2hvID0gVH0KY21wMSA8LSBzeXN0ZW0udGltZShmbGlnaHRzRnJhbWVfRFRbbW9udGggPiA2XSkKY21wMiA8LSBzeXN0ZW0udGltZShmaWx0ZXIoZmxpZ2h0c0ZyYW1lX0RULCBtb250aCA+IDYpKQpwcmludCgie2RhdGEudGFibGV9OiIpCnByaW50KGNtcDEpCnByaW50KCJ7ZHBseXJ9OiIpCnByaW50KGNtcDIpCmBgYAoKRmlsdGVyIHVzaW5nIG11bHRpcGxlIGNvbmRpdGlvbnMKCmBgYHtyIGVjaG8gPSBUfQpjbXAxIDwtIHN5c3RlbS50aW1lKGZsaWdodHNGcmFtZV9EVFttb250aCA9PSAxICYgZGVwX2RlbGF5ID4gMF0pCmNtcDIgPC0gc3lzdGVtLnRpbWUoZmlsdGVyKGZsaWdodHNGcmFtZV9EVCwgbW9udGggPiAxICYgZGVwX2RlbGF5ID4gMCkpCnByaW50KCJ7ZGF0YS50YWJsZX06IikKcHJpbnQoY21wMSkKcHJpbnQoIntkcGx5cn06IikKcHJpbnQoY21wMikKYGBgCgpTb3J0aW5nIHJvd3M6CgpgYGB7ciBlY2hvID0gVH0KY21wMSA8LSBzeXN0ZW0udGltZShmbGlnaHRzRnJhbWVfRFRbb3JkZXIoZGVwX2RlbGF5KV0pCmNtcDIgPC0gc3lzdGVtLnRpbWUoYXJyYW5nZShmbGlnaHRzRnJhbWVfRFQsIGRlcF9kZWxheSkpCnByaW50KCJ7ZGF0YS50YWJsZX06IikKcHJpbnQoY21wMSkKcHJpbnQoIntkcGx5cn06IikKcHJpbnQoY21wMikKYGBgCkVudGVycyBgZGF0YS50YWJsZTo6c2V0a2V5KClgCgpgYGB7ciBlY2hvID0gVH0Kc2V0a2V5KGZsaWdodHNGcmFtZV9EVCwgZGVwX2RlbGF5KQprZXkoZmxpZ2h0c0ZyYW1lX0RUKQpgYGAKCmBgYHtyIGVjaG8gPSBUfQpjbXAxIDwtIHN5c3RlbS50aW1lKGZsaWdodHNGcmFtZV9EVFtvcmRlcihkZXBfZGVsYXkpXSkKY21wMiA8LSBzeXN0ZW0udGltZShhcnJhbmdlKGZsaWdodHNGcmFtZV9EVCwgZGVwX2RlbGF5KSkKcHJpbnQoIntkYXRhLnRhYmxlfToiKQpwcmludChjbXAxKQpwcmludCgie2RwbHlyfToiKQpwcmludChjbXAyKQpgYGAKClBsZWFzZSBiZSBhd2FyZSBvZiB0aGUgZmFjdCB0aGF0IHRoZSBgc3lzdGVtLnRpbWUoKWAgcmVzdWx0cyB3aWxsIHZhcnksIGFuZCB0aGF0IG91ciBgZmxpZ2h0c0ZyYW1lX0RUYCBpcyBmYXIgZnJvbSBiZWluZyBvZiBhIHNpemUgY29uc2lkZXJhYmxlIGZvciBjb21wYXJpc29ucyBiZXR3ZWVuIGB7ZGF0YS50YWJsZX1gIGFuZCB7ZHBseXJ9IG9yIGJhc2UgUi4KClNlbGVjdGluZyBjb2x1bW5zIGlzIGRvbmUgaW4gdGhlIGZvbGxvd2luZyB3YXkuIFRvIHJldHVybiBhIGRhdGEudGFibGUgb2JqZWN0IGZyb20gYW4gZXhpc3RpbmcgZGF0YS50YWJsZToKCmBgYHtyIGVjaG8gPSBUfQphcnJfdGltZSA8LSBmbGlnaHRzRnJhbWVfRFRbICwgbGlzdChhcnJfdGltZSldCmNsYXNzKGFycl90aW1lKQpgYGAKVGhlIGZvbGxvd2luZyBhY2NvbXBsaXNoZXMgdGhlIHNhbWUgd2l0aCBgLmAgdXNlZCBhcyBhbiBhbGlhcyBmb3IgYGxpc3QoKWA6CgpgYGB7ciBlY2hvID0gVH0KYXJyX3RpbWUgPC0gZmxpZ2h0c0ZyYW1lX0RUWywgLihhcnJfdGltZSldCmNsYXNzKGFycl90aW1lKQpgYGAKVG8gb2J0YWluIGEgdmVjdG9yIGZyb20gYSBkYXRhLnRhYmxlIGNvbHVtbjoKCmBgYHtyIGVjaG8gPSBUfQphcnJfdGltZSA8LSBmbGlnaHRzRnJhbWVfRFRbLCBhcnJfdGltZV0KY2xhc3MoYXJyX3RpbWUpCmBgYApPcjoKCmBgYHtyIGVjaG8gPSBUfQphcnJfdGltZSA8LSBmbGlnaHRzRnJhbWVfRFRbWydhcnJfdGltZSddXQpjbGFzcyhhcnJfdGltZSkKYGBgCk5vdyB3ZSBjYW4gdHJ5IGEgc2ltcGxlIGNvbmp1bmN0aW9uIG9mIGZpbHRlcmluZyBhbmQgc2VsZWN0aW9uIG9wZXJhdGlvbnMgaW4ge2RhdGEudGFibGV9LCBmb3IgZXhhbXBsZToKCmBgYHtyIGVjaG8gPSBUfQpzZWxlY3RlZEZsaWdodHMgPC0gCiAgZmxpZ2h0c0ZyYW1lX0RUW2Fycl90aW1lID4gMTAwLCAuKGFycl90aW1lLCBhcnJfZGVsYXksIGRlc3QpXQpoZWFkKHNlbGVjdGVkRmxpZ2h0cykKYGBgCgpUaGUge2RwbHlyfSBlcXVpdmFsZW50IGlzOgoKYGBge3IgZWNobyA9IFR9CnNlbGVjdGVkRmxpZ2h0c19kcGx5ciA8LSBmbGlnaHRzRnJhbWVfRFQgJT4lIAogIHNlbGVjdChhcnJfdGltZSwgYXJyX2RlbGF5LCBkZXN0KSAlPiUgCiAgZmlsdGVyKGFycl90aW1lID4gMTAwKQpoZWFkKHNlbGVjdGVkRmxpZ2h0c19kcGx5cikKYGBgCgpMZXQncyBjcmVhdGUgbmV3IHZhcmlhYmxlczoKCmBgYHtyIGVjaG8gPSBUfQphcnJfdGltZV9ob3VycyA8LSAKICBmbGlnaHRzRnJhbWVfRFRbYWlyX3RpbWUgPiAyMCwgLihhaXJfdGltZSwgYWlyX3RpbWVfaG91cnMgPSBhaXJfdGltZS82MCldCmhlYWQoYXJyX3RpbWVfaG91cnMpCmBgYAoKRW50ZXJzIGA6PWAgLSB0aGUgY29sdW1uIGFzc2lnbm1lbnQgb3BlcmF0b3IgaW4gYHtkYXRhLnRhYmxlfWAsIHdoaWNoIGFsbG93cyB0byBtb2RpZnkgYSBkYXRhLnRhYmxlIG9iamVjdCAqYnkgcmVmZXJlbmNlKiAodG8gYmUgZXhwbGFpbmVkIGluIHRoZSBTZXNzaW9uKS4gV2Ugd2lsbCBmaXJzdCBmaWx0ZXIgb3V0IGFsbCByb3dzIHdpdGggYW4gYE5BYCB2YWx1ZSBpbiBgYWlyX3RpbWVgOgoKYGBge3IgZWNobyA9IFR9CmRpbShmbGlnaHRzRnJhbWVfRFQpCmZsaWdodHNGcmFtZV9EVCA8LSBmbGlnaHRzRnJhbWVfRFRbIWlzLm5hKGFpcl90aW1lKV0KZGltKGZsaWdodHNGcmFtZV9EVCkKYGBgClVzaW5nIGA6PWAgd2UgY2FuIG1vZGlmeSBhIGRhdGEudGFibGUgb2JqZWN0ICp3aXRob3V0IGhhdmluZyB0byBtYWtlIGEgY29weSBvZiBpdCogLSB3aGljaCBoYXBwZW5zIHdpdGggYm90aCBiYXNlIFIgZGF0YS5mcmFtZSBvYmplY3RzIGFuZCBpbiB7ZHBseXJ9OgoKYGBge3IgZWNobyA9IFR9CmZsaWdodHNGcmFtZV9EVFsgLCBhaXJfdGltZV9ob3VycyA6PSBhaXJfdGltZS82MF0KZmxpZ2h0c0ZyYW1lX0RUWyAsIC4oYWlyX3RpbWUsIGFpcl90aW1lX2hvdXJzKV0KYGBgCgojIyMjIDEuMyB7ZGF0YS50YWJsZX0gZ3JvdXBpbmcgKGBieSA9IGApLCBhZ2dyZWdhdGlvbiwgYW5kIGpvaW5zCgpBIHNpbXBsZSBhZ2dyZWdhdGlvbjoKCmBgYHtyIGVjaG8gPSBUfQpmbGlnaHRzRnJhbWVfRFRbICwgLihhdmdfYWlyX3RpbWUgPSBtZWFuKGFpcl90aW1lKSksIGJ5ID0gZGVzdF0KYGBgClRoaXMgaXMgZXF1aXZhbGVudCB0byB7ZHBseXJ9OgoKYGBge3IgZWNobyA9IFR9CmZsaWdodHNGcmFtZV9EVCAlPiUgCiAgc2VsZWN0KGRlc3QsIGFpcl90aW1lKSAlPiUgCiAgZ3JvdXBfYnkoZGVzdCkgJT4lIAogIHN1bW1hcmlzZShhdmdfYWlyX3RpbWUgPSBtZWFuKGFpcl90aW1lKSkKYGBgCgpTaW1wbGUgYWdncmVnYXRpb24gd2l0aCBmaWx0ZXJpbmc6CgpgYGB7ciBlY2hvID0gVH0KZmxpZ2h0c0ZyYW1lX0RUW2RlcF9kZWxheSA8PSAwLCAKICAgICAgICAgICAgICAgIC4oYXZnX2RlcF9kZWxheSA9IG1lYW4oZGVwX2RlbGF5KSwgYXZnX2Fpcl90aW1lID0gbWVhbihhaXJfdGltZSkpLCAKICAgICAgICAgICAgICAgIGJ5ID0gZGVzdF0KYGBgCkNvdW50IHJvd3MgKG9ic2VydmF0aW9ucykgYnkgZ3JvdXBzOgoKYGBge3IgZWNobyA9IFR9CmZsaWdodHNGcmFtZV9EVFtkZXBfZGVsYXkgPD0gMCwgLk4sIGJ5ID0gZGVzdF0KYGBgCgpUbyBkZW1vbnN0cmF0ZSBhIGpvaW4gb3BlcmF0aW9uIGluIHtkYXRhLnRhYmxlfSBsb2FkIHRoZSBgcGxhbmVzYCBkYXRhLmZyYW1lIGZyb20gYG55Y2ZsaWdodHMxM2A6CgpgYGB7ciBlY2hvID0gVH0KcGxhbmVzIDwtIG55Y2ZsaWdodHMxMzo6cGxhbmVzCmhlYWQocGxhbmVzKQpgYGAKCkFzIGEgcmVtaW5kZXIsIHRoaXMgaXMgaG93IGl0IHdvdWxkIGJlIGRvbmUgaW4gYHtkcGx5cn1gIHdpdGggYGxlZnRfam9pbigpYCBvdmVyIGB0YWlsbnVtYCAoc2VlIFNlc3Npb24gMTApOgoKYGBge3IgZWNobyA9IFR9CmZsaWdodHNfcmVsYXRpb25zIDwtIGRwbHlyOjpsZWZ0X2pvaW4oZmxpZ2h0c0ZyYW1lX0RULAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHBsYW5lcywKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBieSA9ICJ0YWlsbnVtIikKYGBgCgpJbiB7ZGF0YS50YWJsZX0sIGZpcnN0IHVzZSBgc2V0a2V5KClgLCB0aGVuIHByb21vdGUgYHBsYW5lc2AgdG8gYSBkYXRhLnRhYmxlIG9iamVjdCwgYW5kIGZpbmFsbHkgdXNlIGBtZXJnZSgpYDoKCmBgYHtyIGVjaG8gPSBUfQpzZXRrZXkoZmxpZ2h0c0ZyYW1lX0RULCB0YWlsbnVtKQpwbGFuZXMgPC0gZGF0YS50YWJsZShwbGFuZXMpCnNldGtleShwbGFuZXMsIHRhaWxudW0pCmZsaWdodFBsYW5lcyA8LSBtZXJnZShmbGlnaHRzRnJhbWVfRFQsCiAgICAgICAgICAgICAgICAgICAgICBwbGFuZXMsCiAgICAgICAgICAgICAgICAgICAgICBieSA9ICJ0YWlsbnVtIiwKICAgICAgICAgICAgICAgICAgICAgIGFsbC54ID0gVCkKYGBgCgpPaC4gT25lIGZpbmFsIHRoaW5nLi4uIGBkYXRhLnRhYmxlOjpmd3JpdGUoKWAuCgpgYGB7ciBlY2hvID0gVH0Kc3lzdGVtLnRpbWUoCiAgd3JpdGUuY3N2KGZsaWdodHNGcmFtZV9EVCwgcGFzdGUwKGRhdGFEaXIsICJmbGlnaHRzRnJhbWVfRFRfd3JpdGVjc3YuY3N2IikpCiAgKQpzeXN0ZW0udGltZSgKICBmd3JpdGUoZmxpZ2h0c0ZyYW1lX0RULCBwYXN0ZTAoZGF0YURpciwgImZsaWdodHNGcmFtZV9EVF9md3JpdGUuY3N2IikpCiAgKQpgYGAKCiMjIyA0LiBDb25kaXRpb25hbCBQcm9iYWJpbGl0eSBhbmQgQmF5ZXMnIFRoZW9yZW0KCiMjIyA0LjEgQ29uZGl0aW9uYWwgUHJvYmFiaWxpdHkKCkltYWdpbmUgdGhlIGZvbGxvd2luZyBzaXR1YXRpb246IHRoZXJlIGFyZSB0d28gcG9wdWxhciBzb2NpYWwgZ3JvdXBzLCBgQWAgYW5kIGBCYCwgYW5kIHdlIGhhdmUgdGhlIGtub3dsZWRnZSBvbiBob3cgbWFueSBtZW4gYW5kIHdvbWVuIGFyZSBtZW1iZXJzIG9mIHRoZW06IGluIGdyb3VwIGBBYCB3ZSBmaW5kIDg3IG1lbiBhbmQgNTcgd29tZW4sIHdoaWxlIGluIGdyb3VwIGBCYCB3ZSBmaW5kIDU3IG1lbiBhbmQgOTYgd29tZW4uCgpgYGB7ciBlY2hvID0gVH0KcHJvYnMgPC0gZGF0YS5mcmFtZShHcm91cCA9IGMoJ0EnLCAnQicpLAogICAgICAgICAgICAgICAgICAgIE1hbGUgPSBjKDg3LCA0NCksIAogICAgICAgICAgICAgICAgICAgIEZlbWFsZSA9IGMoNTcsIDk2KSkKcHJvYnMKYGBgCgpOb3cgaW1hZ2luZSB0aGF0IHdlIHdhbnQgdG8gbWFrZSBvbmUgcmFuZG9tIGRyYXcgZnJvbSB0aGlzIHNhbXBsZSBhbmQgaWRlbnRpZnkgb25lIHNpbmdsZSBpbmRpdmlkdWFsLiBXaGF0IGlzIHRoZSBwcm9iYWJpbGl0eSB0byByYW5kb21seSBzZWxlY3QgYSBtYW4gdnMgYSB3b21hbj8KCkxldCdzIHNlZTogd2UgaGF2ZSAKCmBgYHtyIGVjaG8gPSBUfQptZW4gPC0gc3VtKHByb2JzJE1hbGUpCnByaW50KG1lbikKYGBgCm1lbiwgYW5kIAoKYGBge3IgZWNobyA9IFR9CndvbWVuIDwtIHN1bShwcm9icyRGZW1hbGUpCnByaW50KHdvbWVuKQpgYGAKd29tZW4sIHNvIHRoZSBwcm9iYWJpbGl0eSBgUChtYW4pYCB3b3VsZCBiZToKCmBgYHtyIGVjaG8gPSBUfQpwX21hbiA8LSBtZW4vKG1lbiArIHdvbWVuKQpwX21hbgpgYGAKd2hlcmUgYG1lbiArIHdvbWVuYCBpcyBhbHNvIHRoZSB0b3RhbCBzYW1wbGUgc2l6ZS4gVGhlIHByb2JhYmlsaXR5IHRvIHJhbmRvbWx5IGRyYXcgYSB3b21hbiBmcm9tIHRoZSBzYW1wbGUgaXM6CgpgYGB7ciBlY2hvID0gVH0KcF93b21hbiA8LSB3b21lbi8obWVuICsgd29tZW4pCnBfd29tYW4KYGBgCk9mIGNvdXJzZSwKCmBgYHtyIGVjaG8gPSBUfQpwX21hbiArIHBfd29tYW4KYGBgCk5vdzogd2hhdCB3b3VsZCBiZSB0aGUgcHJvYmFiaWxpdHkgdG8gcmFuZG9tbHkgZHJhdyBhIG1hbiBmcm9tIHRoZSBzYW1wbGUgKippZiB3ZSBhbHJlYWR5IGtub3cqKiB0aGF0IHdlIHdpbGwgYmUgcGlja2luZyBzb21lb25lIGZyb20gZ3JvdXAgYEFgPyBMZXQncyB0YWtlIGEgbG9vayBhdCB0aGUgc2FtcGxlIG9uY2UgYWdhaW46CgpgYGB7ciBlY2hvID0gVH0KcHJvYnMKYGBgCgpPYnZpb3VzbHksIHdlIHdvdWxkIGZvY3VzIG91ciBhdHRlbnRpb24gb24gdGhlIGZpcnN0IHJvdyBvbmx5LCB3aGVyZSB3ZSBmaW5kIDg3IG1lbiBhbmQgNTcgd29tZW4sIG5lZ2xlY3RpbmcgdGhlIHNlY29uZCByb3cgYmVjYXVzZSB3ZSBhbHJlYWR5IGtub3cgdGhhdCB0aGUgcGVyc29uIGlzIGEgbWVtYmVyIG9mIGdyb3VwIGBBYC4KCmBgYHtyIGVjaG8gPSBUfQpjcF9tYW5fQSA8LSBwcm9icyRNYWxlW3Byb2JzJEdyb3VwID09ICdBJ10vKHByb2JzJE1hbGVbcHJvYnMkR3JvdXAgPT0gJ0EnXSArIHByb2JzJEZlbWFsZVtwcm9icyRHcm91cCA9PSAnQSddKQpjcF9tYW5fQQpgYGAKCmBgYHtyIGVjaG8gPSBUfQpjcF93b21hbl9BIDwtIAogIHByb2JzJEZlbWFsZVtwcm9icyRHcm91cCA9PSAnQSddLyhwcm9icyRNYWxlW3Byb2JzJEdyb3VwID09ICdBJ10gKyBwcm9icyRGZW1hbGVbcHJvYnMkR3JvdXAgPT0gJ0EnXSkKY3Bfd29tYW5fQQpgYGAKQW5kIGFnYWluIHdlIGhhdmU6CgpgYGB7ciBlY2hvID0gVH0KY3Bfd29tYW5fQSArIGNwX21hbl9BCmBgYApUaGUgcHJvYmFiaWxpdGllcyBgY3BfbWFuX0FgIGFuZCBgY3Bfd29tYW5fQWAgYXJlIGNhbGxlZCAqKmNvbmRpdGlvbmFsIHByb2JhYmlsaXRpZXMqKiBhbmQgcGxheSBhIHZlcnkgaW1wb3J0YW50IHJvbGUgaW4gbWFueSBtYXRoZW1hdGljYWwgbW9kZWxzIHVzZWQgaW4gRGF0YSBTY2llbmNlIGFuZCBNYWNoaW5lIExlYXJuaW5nOgoKJFAoWXxYKSA9IFxmcmFje1AoWXtcY2FwfVgpfXtQKFgpfSQKCndoZXJlICRQKFl8WCkkIGlzIHRoZSBjb25kaXRpb25hbCBwcm9iYWJpbGl0eSBvZiBvYnNlcnZpbmcgWSBnaXZlbiB0aGF0IHdlIGFscmVhZHkga25vdyB0aGF0IFggb2J0YWlucywgd2hpbGUgJHtQKFh7XGNhcH1ZKX0kIGlzIHRoZSAqam9pbnQgcHJvYmFiaWxpdHkqIG9mIG9ic2VydmluZyBib3RoIFggYW5kIFkuIExldCcgc2VlOiB3aGF0IGlzIHRoZSBqb2ludCBwcm9iYWJpbGl0eSBvZiBvYnNlcnZpbmcgYm90aCBhIHBlcnNvbiBmcm9tIGdyb3VwIEEgKmFuZCogYSBtYW4/IEZyb20gdGhlIHRhYmxlIHdlIHNlZSB0aGF0IHRoZXJlIGFyZSA4NyBtZW4gaW4gZ3JvdXAgQSwgc28gdGhhdCBwcm9iYWJpbGl0eSBtdXN0IGJlIDg3IGRpdmlkZWQgYnkgdGhlIHRvdGFsIHNhbXBsZSBzaXplIHdoaWNoIGlzIGByIHN1bShwcm9ic1ssIDI6M10pYDoKCmBgYHtyIGVjaG8gPSBUfQpwcm9icyRNYWxlWzFdL3N1bShwcm9ic1ssIDI6M10pCmBgYApOb3cgd2UgbmVlZCB0byBkaXZpZGUgdGhpcyBqb2ludCBwcm9iYWJpbGl0eSBieSBgUChBKWAgaW4gdG90YWw6CgpgYGB7ciBlY2hvID0gVH0Kc3VtKHByb2JzWzEsIDI6M10pL3N1bShwcm9ic1ssIDI6M10pCmBgYAphbmQgdGhlIGRlc2lyZWQgY29uZGl0aW9uYWwgcHJvYmFiaWxpdHkgaXM6CgpgYGB7ciBlY2hvID0gVH0Kam9pbnRQIDwtIHByb2JzJE1hbGVbMV0vc3VtKHByb2JzWywgMjozXSkKdG90YWxQIDwtIHN1bShwcm9ic1sxLCAyOjNdKS9zdW0ocHJvYnNbLCAyOjNdKQpqb2ludFAvdG90YWxQCmBgYAojIyMgNC4yIEluZGVwZW5kZW5jZQoKVHdvIGV2ZW50cywgJFgkIGFuZCAkWSQsIGFyZSBzYWlkIHRvIGJlICpzdGF0aXN0aWNhbGx5IGluZGVwZW5kZW50KiBpZmY6CgokJFAoWHtcY2FwfVkpID0gUChYKVAoWSkkJAoKSWYgJFAoWCkkIGlzIG5vdCB6ZXJvLCBhbmQgZ2l2ZW4gdGhhdAoKJCRQKFl8WCkgPSBcZnJhY3tQKFl7XGNhcH1YKX17UChYKX0kJCwKCnRoYXQgbWVhbnMgdGhhdCBzdGF0aXN0aWNhbCBpbmRlcGVuZGVuY2UgaW1wbGllcyBzb21ldGhpbmcgdmVyeSBpbnR1aXRpdmUsIG5hbWVseToKCiQkUChZfFgpID0gUChZKSQkCgojIyMgNC4zIEJheWVzJyBUaGVvcmVtCgpJdCBjYW4gYmUgZWFzaWx5IFtzaG93bl0oaHR0cHM6Ly9lbi53aWtpcGVkaWEub3JnL3dpa2kvQmF5ZXMlMjdfdGhlb3JlbSNQcm9vZikgdGhhdCB0aGUgZm9sbG93aW5nIGhvbGRzOgoKJCRQKFl8WCkgPSBcZnJhY3tQKFh8WSlQKFkpfXtQKFgpfSQkCgpUaGUgZXhwcmVzc2lvbiBpcyBjYWxsZWQgW0JheWVzJyBUaGVvcmVtXShodHRwczovL2VuLndpa2lwZWRpYS5vcmcvd2lraS9CYXllcyUyN190aGVvcmVtKSBhbmQgcGxheXMgYSByb2xlIG9mICoqaW1tZW5zZSBpbXBvcnRhbmNlKiogaW4gY29udGVtcG9yYXJ5IG1hdGhlbWF0aWNhbCBzdGF0aXN0aWNzLCBEYXRhIFNjaWVuY2UgYW5kIE1hY2hpbmUgTGVhcm5pbmcuCgpUaGlzIGlzIGhvdyB3ZSBzcGVhayBvZiBpdHMgdGVybXM6CgokJFAoWXxYKSQkCgppcyB0aGUgKipwb3N0ZXJpb3IgcHJvYmFiaWxpdHkqKiBvZiBvYnRhaW5pbmcgJFkkIGZyb20ga25vd2luZyAkWCQKCiQkUChYfFkpJCQKCmlzIHRoZSAqKmxpa2VsaWhvb2QqKiBvZiBvYnRhaW5pbmcgJFgkIGZyb20ga25vd2luZyAkWSQKCiQkUChZKSQkCgppcyB0aGUgKipwcmlvciBwcm9iYWJpbGl0eSoqIG9mIG9idGFpbmluZyAkWSQuCgojIyMgNC40IEJheWVzaWFuIEluZmVyZW5jZSBmb3IgYSBCaW5vbWlhbCBEaXN0cmlidXRpb24gZnJvbSBhIEJldGEgUHJpb3IKCkltYWdpbmUgdGhhdCB3ZSB3aXNoIHRvIGV4cHJlc3Mgb3VyICpiZWxpZWYqIGFib3V0IHRoZSBwYXJhbWV0ZXIgJHAkIG9mIGEgQmlub21pYWwgRGlzdHJpYnV0aW9uIGJ5IGFub3RoZXIgZnVuY3Rpb24uIEl0IGNhbiBiZSBzaG93biB0aGF0IGl0IG1ha2VzIHNlbnNlIHRvIHVzZSB0aGUgW0JldGEgRGlzdHJpYnV0aW9uXShodHRwczovL2VuLndpa2lwZWRpYS5vcmcvd2lraS9CZXRhX2Rpc3RyaWJ1dGlvbikgdG8gZXhwcmVzcyBzdWNoIGJlbGllZnMsIHdoaWNoIGhhcyBhIHN1cHBvcnQgY29uc3RyYWluZWQgdG8gWzAsIDFdOgoKJCRQKHg7XGFscGhhLCBcYmV0YSkgPSBcZnJhY3t4XntcYWxwaGEtMX0oMS14KV57XGJldGEtMX19e0IoXGFscGhhLFxiZXRhKX0kJAoKd2hlcmUgJEIoXGFscGhhLFxiZXRhKSQgaXMgdGhlIEJldGEgZnVuY3Rpb24KCiQkQihcYWxwaGEsXGJldGEpPVxmcmFje1xHYW1tYShcYWxwaGEpXEdhbW1hKFxiZXRhKX17XEdhbW1hKFxhbHBoYSkrXEdhbW1hKFxiZXRhKX0kJAoKYW5kIHNlcnZlcyB0aGUgcHVycG9zZXMgb2Ygbm9ybWFsaXphdGlvbiBvbmx5LgoKRm9yIGV4YW1wbGUsIGEgJEJldGEoeDtcYWxwaGEsXGJldGEpJCBkaXN0cmlidXRpb24gd2l0aCAkXGFscGhhID0gNyQgYW5kICRcYmV0YT0xMyQgbG9va3MgbGlrZSB0aGlzOgoKYGBge3IgZWNobyA9IFR9CmxpYnJhcnkoZ2dwbG90MikKeCA8LSBzZXEoLjAxLCAuOTksIGJ5ID0gLjAxKQpkZW5zaXR5IDwtIGRiZXRhKHgsIDcsIDEzKQpkZW5zRnJhbWUgPC0gZGF0YS5mcmFtZSh4ID0geCwgCiAgICAgICAgICAgICAgICAgICAgICAgIGRlbnNpdHkgPSBkZW5zaXR5KQpnZ3Bsb3QoZGVuc0ZyYW1lLCBhZXMoeCA9IHgsIHkgPSBkZW5zaXR5KSkgKyAKICBnZW9tX3BhdGgoc2l6ZSA9IC4zNSwgZ3JvdXAgPSAxLCBjb2xvciA9ICJyZWQiKSArIAogIGdndGl0bGUoIkJldGEoMyw3KSIpICsgCiAgdGhlbWVfYncoKSArIAogIHRoZW1lKHBhbmVsLmJvcmRlciA9IGVsZW1lbnRfYmxhbmsoKSkKYGBgCgpBbmQgdGhpcyBpcyBob3cgJEJldGEoeDtcYWxwaGEsXGJldGEpJCBkaXN0cmlidXRpb24gd2l0aCAkXGFscGhhID0gMSQgYW5kICRcYmV0YT0xJCAoaS5lLiB0aGUgVW5pZm9ybSBQcmlvcikgbG9va3MgbGlrZQoKYGBge3IgZWNobyA9IFR9CnggPC0gc2VxKC4wMSwgLjk5LCBieSA9IC4wMSkKZGVuc2l0eSA8LSBkYmV0YSh4LCAxLCAxKQpkZW5zRnJhbWUgPC0gZGF0YS5mcmFtZSh4ID0geCwgCiAgICAgICAgICAgICAgICAgICAgICAgIGRlbnNpdHkgPSBkZW5zaXR5KQpnZ3Bsb3QoZGVuc0ZyYW1lLCBhZXMoeCA9IHgsIHkgPSBkZW5zaXR5KSkgKyAKICBnZW9tX3BhdGgoc2l6ZSA9IC4zNSwgZ3JvdXAgPSAxLCBjb2xvciA9ICJyZWQiKSArIAogIGdndGl0bGUoIkJldGEoMSwxKSIpICsgCiAgdGhlbWVfYncoKSArIAogIHRoZW1lKHBhbmVsLmJvcmRlciA9IGVsZW1lbnRfYmxhbmsoKSkKYGBgCgpXaGlsZSAkQmV0YSh4O1xhbHBoYSxcYmV0YSkkIGRpc3RyaWJ1dGlvbiB3aXRoICRcYWxwaGEgPSAxLzIkIGFuZCAkXGJldGE9MS8yJCBsb29rcyBsaWtlIHRoaXMgKHRoaXMgaXMgYWxzbyBjYWxsZWQgYSBKZWZmcmV5J3MgUHJpb3IpOgoKYGBge3IgZWNobyA9IFR9CnggPC0gc2VxKC4wMSwgLjk5LCBieSA9IC4wMSkKZGVuc2l0eSA8LSBkYmV0YSh4LCAxLzIsIDEvMikKZGVuc0ZyYW1lIDwtIGRhdGEuZnJhbWUoeCA9IHgsIAogICAgICAgICAgICAgICAgICAgICAgICBkZW5zaXR5ID0gZGVuc2l0eSkKZ2dwbG90KGRlbnNGcmFtZSwgYWVzKHggPSB4LCB5ID0gZGVuc2l0eSkpICsgCiAgZ2VvbV9wYXRoKHNpemUgPSAuMzUsIGdyb3VwID0gMSwgY29sb3IgPSAicmVkIikgKyAKICBnZ3RpdGxlKCJCZXRhKDEvMiwxLzIpIikgKyAKICB0aGVtZV9idygpICsgCiAgdGhlbWUocGFuZWwuYm9yZGVyID0gZWxlbWVudF9ibGFuaygpKQpgYGAKClR1cm5zIG91dCB0aGF0IHRoZSAkQmV0YShcYWxwaGEsIFxiZXRhKSQgZGlzdHJpYnV0aW9uIGhhcyBhIG5pY2UgcHJvcGVydHkgdGhhdCBtYWtlcyBpdCBhIHN1aXRhYmxlICoqY29uanVnYXRlIHByaW9yIGRpc3RyaWJ1dGlvbioqIGluIEJheWVzaWFuIGluZmVyZW5jZSBmb3IgdGhlIEJpbm9taWFsIERpc3RyaWJ1dGlvbi4gTmFtZWx5LCBpZiBvdXIgcHJpb3IgYmVsaWVmcyBhYm91dCB0aGUgQmlub21pYWwgJHAkIHBhcmFtZXRlciBhcmUgZXhwcmVzc2VkIGFzICRCZXRhKHA7IFxhbHBoYSwgXGJldGEpJCwgYW5kIHRoZW4gd2Ugb2JzZXJ2ZSAkeCQgc3VjY2Vzc2VzIGZyb20gJG4kIHRyaWFscyBpbiBhIEJpbm9taWFsIGV4cGVyaW1lbnQsIG91ciBwb3N0ZXJpb3IgYmVsaWVmIGFib3V0IHRoZSBCaW5vbWlhbCAkcCQgcGFyYW1ldGVyIGNhbiBiZSBleHByZXNzZWQgYXM6CgokJEJldGEocDtcYWxwaGEnLFxiZXRhJykkJAp3aGVyZSAKCiQkXGFscGhhJyA9IFxhbHBoYSArIHgkJAoKYW5kIAoKJCRcYmV0YScgPSBcYmV0YSArIG4gLSB4JCQKTGV0J3MgaWxsdXN0cmF0ZS4gU2F5IHRoYXQgaW4gdGhlIGJlZ2lubmluZyB3ZSBrbm93IG5vdGhpbmcgYWJvdXQgdGhlIHBvc3NpYmxlIHZhbHVlIG9mICRwJC4gV2UgZXhwcmVzcyB0aGlzIGFic2VuY2Ugb2Yga25vd2xlZGdlIGJ5IGEgKip1bmlmb3JtIHByaW9yKiosICRCZXRhKFxhbHBoYT0xLCBcYmV0YT0xKSkkOgoKYGBge3IgZWNobyA9IFR9CnggPC0gc2VxKC4wMSwgLjk5LCBieSA9IC4wMSkKZGVuc2l0eSA8LSBkYmV0YSh4LCAxLCAxKQpkZW5zRnJhbWUgPC0gZGF0YS5mcmFtZShwID0geCwgCiAgICAgICAgICAgICAgICAgICAgICAgIGRlbnNpdHkgPSBkZW5zaXR5KQpnZ3Bsb3QoZGVuc0ZyYW1lLCBhZXMoeCA9IHAsIHkgPSBkZW5zaXR5KSkgKyAKICBnZW9tX3BhdGgoc2l6ZSA9IC4zNSwgZ3JvdXAgPSAxLCBjb2xvciA9ICJyZWQiKSArIAogIGdndGl0bGUoIk91ciBhIHByaW9yaSBpczogQmV0YSgxLDEpIikgKyAKICB0aGVtZV9idygpICsgCiAgdGhlbWUocGFuZWwuYm9yZGVyID0gZWxlbWVudF9ibGFuaygpKQpgYGAKCkxldCdzIGFzc3VtZSB0aGF0IHRoYW4gd2Ugb2JzZXJ2ZSAxLDAwMCBjb2luIHRvc3NlcyBvZiB3aGljaCAyNzUgcmVzdWx0ZWQgaW4gc3VjY2VzcyAoaS5lLiAkSGVhZCQpOyB3ZSB1cGRhdGUgb3VyIHByaW9yIGJlbGllZnMgYWNjb3JkaW5nbHk6CgpgYGB7ciBlY2hvID0gVH0KbGlicmFyeSh0aWR5cikKeCA8LSBzZXEoLjAxLCAuOTksIGJ5ID0gLjAxKQojIC0gcHJpb3IKYWxwaGEgPC0gMQpiZXRhIDwtIDEKZGVuc2l0eSA8LSBkYmV0YSh4LCBhbHBoYSwgYmV0YSkKIyAtIHVwZGF0ZQpwb3N0X2FscGhhIDwtIGFscGhhICsgMjc1CnBvc3RfYmV0YSA8LSBiZXRhICsgMTAwMCAtIDI3NQpwb3N0X2RlbnNpdHkgPC0gZGJldGEoeCwgcG9zdF9hbHBoYSwgcG9zdF9iZXRhKSAKZGVuc0ZyYW1lIDwtIGRhdGEuZnJhbWUocCA9IHgsIAogICAgICAgICAgICAgICAgICAgICAgICBwcmlvciA9IGRlbnNpdHksCiAgICAgICAgICAgICAgICAgICAgICAgIHBvc3RlcmlvciA9IHBvc3RfZGVuc2l0eSkgJT4lIAogIHRpZHlyOjpwaXZvdF9sb25nZXIoLXAsIAogICAgICAgICAgICAgICAgICAgICAgbmFtZXNfdG8gPSAiYmVsaWVmcyIsCiAgICAgICAgICAgICAgICAgICAgICB2YWx1ZXNfdG8gPSAidmFsdWUiKQpnZ3Bsb3QoZGVuc0ZyYW1lLCBhZXMoeCA9IHAsIAogICAgICAgICAgICAgICAgICAgICAgeSA9IHZhbHVlLAogICAgICAgICAgICAgICAgICAgICAgZ3JvdXAgPSBiZWxpZWZzLCAKICAgICAgICAgICAgICAgICAgICAgIGNvbG9yID0gYmVsaWVmcykpICsgCiAgZ2VvbV9wYXRoKHNpemUgPSAuMzUsICkgKyAKICBnZ3RpdGxlKCJQcmlvciBhbmQgUG9zdGVyaW9yIikgKyAKICB0aGVtZV9idygpICsgCiAgdGhlbWUocGFuZWwuYm9yZGVyID0gZWxlbWVudF9ibGFuaygpKQpgYGAKCkFuZCB3aGF0IGlmIHdlIGhhdmUgc29tZSBwcmlvciBrbm93bGVnZGUgYW5kIGRvIG5vdCB3aXNoIHRvIGJlZ2luIGZyb20gYSB1bmlmb3JtIHByaW9yIGRpc3RyaWJ1dGlvbj8gV2hhdCBpZiB3ZSBoYXZlIGFscmVhZHkgb2JzZXJ2ZWQgMSwwMDAgY29pbiB0b3NzZXMgdGhhdCByZXN1bHRlZCBpbiAyNzUgaGVhZHMsIGFuZCB0aGVuIG9ubHkgd2Ugb2JzZXJ2ZSBhbm90aGVyIDUwMCB0b3NzZXMgcmVzdWx0aW5nIGluIDE1MCBoZWFkcyAod2VpcmQsIGJ1dCBzdGlsbCk/CgpgYGB7ciBlY2hvID0gVH0KbGlicmFyeSh0aWR5cikKeCA8LSBzZXEoLjAxLCAuOTksIGJ5ID0gLjAxKQojIC0gcHJpb3IKYWxwaGEgPC0gMjc2CmJldGEgPC0gNzI2CmRlbnNpdHkgPC0gZGJldGEoeCwgYWxwaGEsIGJldGEpCiMgLSB1cGRhdGUKcG9zdF9hbHBoYSA8LSBhbHBoYSArIDE1MApwb3N0X2JldGEgPC0gYmV0YSArIDUwMCAtIDE1MApwb3N0X2RlbnNpdHkgPC0gZGJldGEoeCwgcG9zdF9hbHBoYSwgcG9zdF9iZXRhKSAKZGVuc0ZyYW1lIDwtIGRhdGEuZnJhbWUocCA9IHgsIAogICAgICAgICAgICAgICAgICAgICAgICBwcmlvciA9IGRlbnNpdHksCiAgICAgICAgICAgICAgICAgICAgICAgIHBvc3RlcmlvciA9IHBvc3RfZGVuc2l0eSkgJT4lIAogIHRpZHlyOjpwaXZvdF9sb25nZXIoLXAsIAogICAgICAgICAgICAgICAgICAgICAgbmFtZXNfdG8gPSAiYmVsaWVmcyIsCiAgICAgICAgICAgICAgICAgICAgICB2YWx1ZXNfdG8gPSAidmFsdWUiKQpnZ3Bsb3QoZGVuc0ZyYW1lLCBhZXMoeCA9IHAsIAogICAgICAgICAgICAgICAgICAgICAgeSA9IHZhbHVlLAogICAgICAgICAgICAgICAgICAgICAgZ3JvdXAgPSBiZWxpZWZzLCAKICAgICAgICAgICAgICAgICAgICAgIGNvbG9yID0gYmVsaWVmcykpICsgCiAgZ2VvbV9wYXRoKHNpemUgPSAuMzUsICkgKyAKICBnZ3RpdGxlKCJQcmlvciBhbmQgUG9zdGVyaW9yIikgKyAKICB0aGVtZV9idygpICsgCiAgdGhlbWUocGFuZWwuYm9yZGVyID0gZWxlbWVudF9ibGFuaygpKQpgYGAKCiMjIyBGdXJ0aGVyIFJlYWRpbmdzCgotIFtJbnRyb2R1Y3Rpb24gdG8gZGF0YS50YWJsZTogVmlnbmV0dGVdKGh0dHBzOi8vY3Jhbi5yLXByb2plY3Qub3JnL3dlYi9wYWNrYWdlcy9kYXRhLnRhYmxlL3ZpZ25ldHRlcy9kYXRhdGFibGUtaW50cm8uaHRtbCkKLSBbQSBkYXRhLnRhYmxlIGFuZCBkcGx5ciB0b3VyXShodHRwczovL2F0cmViYXMuZ2l0aHViLmlvL3Bvc3QvMjAxOS0wMy0wMy1kYXRhdGFibGUtZHBseXIvKQotIFtCYXllcycgVGhlb3JlbSwgZnJvbSBFbmdsaXNoIFdpa2lwZWRpYV0oaHR0cHM6Ly9lbi53aWtpcGVkaWEub3JnL3dpa2kvQmF5ZXMlMjdfdGhlb3JlbSkKCgojIyMgUiBNYXJrZG93bgoKW1IgTWFya2Rvd25dKGh0dHBzOi8vcm1hcmtkb3duLnJzdHVkaW8uY29tLykgaXMgd2hhdCBJIGhhdmUgdXNlZCB0byBwcm9kdWNlIHRoaXMgYmVhdXRpZnVsIE5vdGVib29rLiBXZSB3aWxsIGxlYXJuIG1vcmUgYWJvdXQgaXQgbmVhciB0aGUgZW5kIG9mIHRoZSBjb3Vyc2UsIGJ1dCBpZiB5b3UgYWxyZWFkeSBmZWVsIHJlYWR5IHRvIGRpdmUgZGVlcCwgaGVyZSdzIGEgYm9vazogW1IgTWFya2Rvd246IFRoZSBEZWZpbml0aXZlIEd1aWRlLCBZaWh1aSBYaWUsIEouIEouIEFsbGFpcmUsIEdhcnJldHQgR3JvbGVtdW5kcy5dKGh0dHBzOi8vYm9va2Rvd24ub3JnL3lpaHVpL3JtYXJrZG93bi8pIAoKCioqKgpHb3JhbiBTLiBNaWxvdmFub3ZpxIcKCkRhdGFLb2xla3RpdiwgMjAyMC8yMQoKY29udGFjdDogZ29yYW4ubWlsb3Zhbm92aWNAZGF0YWtvbGVrdGl2LmNvbQoKIVtdKC4uL19pbWcvREtfTG9nb18xMDAucG5nKQoKKioqCkxpY2Vuc2U6IFtHUEx2M10oaHR0cDovL3d3dy5nbnUub3JnL2xpY2Vuc2VzL2dwbC0zLjAudHh0KQpUaGlzIE5vdGVib29rIGlzIGZyZWUgc29mdHdhcmU6IHlvdSBjYW4gcmVkaXN0cmlidXRlIGl0IGFuZC9vciBtb2RpZnkgaXQgdW5kZXIgdGhlIHRlcm1zIG9mIHRoZSBHTlUgR2VuZXJhbCBQdWJsaWMgTGljZW5zZSBhcyBwdWJsaXNoZWQgYnkgdGhlIEZyZWUgU29mdHdhcmUgRm91bmRhdGlvbiwgZWl0aGVyIHZlcnNpb24gMyBvZiB0aGUgTGljZW5zZSwgb3IgKGF0IHlvdXIgb3B0aW9uKSBhbnkgbGF0ZXIgdmVyc2lvbi4KVGhpcyBOb3RlYm9vayBpcyBkaXN0cmlidXRlZCBpbiB0aGUgaG9wZSB0aGF0IGl0IHdpbGwgYmUgdXNlZnVsLCBidXQgV0lUSE9VVCBBTlkgV0FSUkFOVFk7IHdpdGhvdXQgZXZlbiB0aGUgaW1wbGllZCB3YXJyYW50eSBvZiBNRVJDSEFOVEFCSUxJVFkgb3IgRklUTkVTUyBGT1IgQSBQQVJUSUNVTEFSIFBVUlBPU0UuICBTZWUgdGhlIEdOVSBHZW5lcmFsIFB1YmxpYyBMaWNlbnNlIGZvciBtb3JlIGRldGFpbHMuCllvdSBzaG91bGQgaGF2ZSByZWNlaXZlZCBhIGNvcHkgb2YgdGhlIEdOVSBHZW5lcmFsIFB1YmxpYyBMaWNlbnNlIGFsb25nIHdpdGggdGhpcyBOb3RlYm9vay4gSWYgbm90LCBzZWUgPGh0dHA6Ly93d3cuZ251Lm9yZy9saWNlbnNlcy8+LgoKKioqCgo=