This project is designed to test your current knowledge on applying several of the skills you learned today (i.e. embeddings, LSTM, functional keras API). The objective is to develop a model that predicts which of the provided pairs of Quora questions contain the same meaning (could be classified as duplicates). The dataset first appeared in the Kaggle competition Quora Question Pairs and consists of approximately 400,000 pairs of questions along with a column indicating if the question pair is considered a duplicate.

After you complete this project, you can read about Quora’s approach to this problem in this blog post.

Good luck!

Package Requirements

library(keras)
library(tidyverse)
library(rsample)
library(testthat)
library(glue)

Import Data

Data can be downloaded from the Kaggle dataset webpage or from Quora’s release of the dataset. The data set should contain 404,290 observations and 6 columns.

quora_data <- get_file(
  "quora_duplicate_questions.tsv",
  "http://qim.fs.quoracdn.net/quora_duplicate_questions.tsv"
) %>%
  read_tsv()

expect_equal(dim(quora_data), c(404290, 6))

⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️️⚠️⚠️⚠️⚠️⚠️⚠️


Often when you are working with large datasets, it is wise to just start with a fraction of the data to do initial model exploration. Once you have a good working model, then you can remove this code chunk and run the analysis on the full dataset. Note that modeling with the full dataset can take multiple hours on a CPU and even close to an hour on a GPU when using LSTM layers.


⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️️⚠️⚠️⚠️⚠️⚠️⚠️
# remove after you have a good working model
set.seed(123)
quora_data <- quora_data %>% sample_frac(0.05)
dim(quora_data)
[1] 20214     6

Our dataset contains six columns. The last three columns are the ones of interest:

quora_data

Data Exploration

We can see that approximately 36% of our question pairs represent questions with similar meaning (aka duplicates). For this project you do not need to worry about resampling procedures to balance the response.

table(quora_data$is_duplicate) %>% prop.table()

        0         1 
0.6359454 0.3640546 

Now let’s assess our questions text. First, we’ll get all unique questions.

unique_questions <- unique(c(quora_data$question1, quora_data$question2))

Let’s take a look at the number of unique words across all our questions. This will helps us to decide the vocabular size, which can be treated as a hyperparameter of our model.

The following code chunk:

  1. splits up all our text into individual words,
  2. does some text cleaning to remove punctuations and normalize the case,
  3. and computes the number of unique words.

We can see that there are 110,842 unique words in the full dataset (23,739 unique words when using our sampled data).

unique_questions %>% 
  str_split(pattern = " ") %>% 
  unlist() %>% 
  str_remove_all(pattern = "[[:punct:]]") %>%
  str_to_lower() %>%
  unique() %>% 
  length()
[1] 23739

Let’s take a look at the number of words in each question. This will helps us to decide the padding length, another hyperparameter of our model. We can see that nearly all questions have 32 or less words.

unique_questions %>%
  map_int(~ str_count(., "\\w+")) %>%
  quantile(c(0.25, 0.5, 0.75, 0.99), na.rm = TRUE)
25% 50% 75% 99% 
  7  10  13  32 

Set basic hyperparameters

Let’s go ahead and set some basic hyperparameters for our model. These are all values that you can adjust if time allows.

vocab_size <- 10000
max_len <- 20
embedding_size <- 256
lstm_size <- 512

Preprocess our text

Next, let’s create a text tokenizer to define how we want to preprocess the text (i.e. convert to lowercase, remove punctuation, token splitting characters) and then apply it to our question text.

tokenizer <- text_tokenizer(num_words = vocab_size) %>%
  fit_text_tokenizer(unique_questions)

Next, let’s create two new objects:

question1 <- texts_to_sequences(tokenizer, quora_data$question1)
question2 <- texts_to_sequences(tokenizer, quora_data$question2)

If you look at the first 6 question1 obs, we see that they are of different length. To create embeddings for these questions, we need to standardize their length. Go ahead and create two new objects (question1_padded & question2_padded) that pad question1 and question2.

The default padding value is 0, but this value is often used for words that don’t appear within the established word index. We could still pad with this value or we could differentiate this value by padding with vocab_size + 1.

question1_padded <- pad_sequences(question1, maxlen = max_len, value = vocab_size + 1)
question2_padded <- pad_sequences(question2, maxlen = max_len, value = vocab_size + 1)

We have now finished the preprocessing steps. We will now run a simple benchmark model before moving on to the Keras model.

Simple benchmark

Before creating a complicated model let’s take a simple approach. Let’s create two predictors: percentage of words from question1 that appear in the question2 and vice-versa. Then we will use a logistic regression to predict if the questions are duplicates.

perc_words_question1 <- map2_dbl(question1, question2, ~mean(.x %in% .y))
perc_words_question2 <- map2_dbl(question2, question1, ~ mean(.x %in% .y))

df_model <- data.frame(
  perc_words_question1 = perc_words_question1,
  perc_words_question2 = perc_words_question2,
  is_duplicate = quora_data$is_duplicate
) %>%
  na.omit()

Now that we have our predictors, let’s create the logistic model. We will take a small sample for validation.

set.seed(123)
index <- sample.int(nrow(df_model), 0.9 * nrow(df_model))
benchmark_train <- df_model[index, ]
benchmark_valid <- df_model[-index, ]

logistic_regression <- glm(
  is_duplicate ~ perc_words_question1 + perc_words_question2, 
  family = "binomial",
  data = benchmark_train
)

summary(logistic_regression)

Call:
glm(formula = is_duplicate ~ perc_words_question1 + perc_words_question2, 
    family = "binomial", data = benchmark_train)

Deviance Residuals: 
    Min       1Q   Median       3Q      Max  
-1.5586  -0.9012  -0.6156   1.1482   2.1661  

Coefficients:
                     Estimate Std. Error z value Pr(>|z|)    
(Intercept)          -2.24544    0.04308  -52.12   <2e-16 ***
perc_words_question1  1.31992    0.09993   13.21   <2e-16 ***
perc_words_question2  1.78807    0.09921   18.02   <2e-16 ***
---
Signif. codes:  0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1

(Dispersion parameter for binomial family taken to be 1)

    Null deviance: 23859  on 18191  degrees of freedom
Residual deviance: 21549  on 18189  degrees of freedom
AIC: 21555

Number of Fisher Scoring iterations: 3

Let’s calculate the accuracy on our validation set.

pred <- predict(logistic_regression, benchmark_valid, type = "response")
pred <- pred > mean(benchmark_valid$is_duplicate)
accuracy <- table(pred, benchmark_valid$is_duplicate) %>% 
  prop.table() %>% 
  diag() %>% 
  sum()

glue("Our benchmark model achieves a {round(accuracy * 100, 2)}% accuracy.")
Our benchmark model achieves a 65.48% accuracy.

Now your goal is to create a keras model that out performs this benchmark.

Modeling with just embeddings

For our first model, we’ll use a similar architecture as we saw in the collaborative filtering notebook ℹ️ with a couple of adjustments. In essence, we want to build a model that:

  1. creates word embeddings for question 1 and 2,
  2. computes the dot product of these embeddings,
  3. adds a dense classifier to predict if the questions are duplicates.

First, create the keras model components by filling in the blanks.

  1. Input layers
    • we need two inputs, one for each question
    • the input layer shape needs to be the same size as the number of columns of our inputs (question1_padded, question2_padded)
  2. Embedding layers
    • build onto our input layers
    • input_dim: in this case input_dim equals vocab_size + 2 since we have the number of words (vocab_size), plus the value 0 represents words not in our declared frequency, and the value vocab_size + 1 represents our padding value.
    • output_dim: represents the desired embeddings dimension (line 154).
    • input_length: represents the length of our inputs (max_len). We need this value because we need to flatten our embeddings for downstream computation.
  3. Flatten layer
    • In the collaborative filtering notebook, we were embedding single input values (i.e. user ID & movie ID). In this example, we are embedding inputs of length 20 (max_len), which results in a matrix. Consequently, we flatten these embedding matrices so that we can compute the dot product.
  4. Dot product
    • The dot product will be computed for our flattened embeddings of question 1 and 2.
    • Since we flattened our embeddings we use axes = 1
  5. Output/prediction
    • Since our response is binary (1 = is duplicate, 0 = not duplicate), we need to use the applicable activation function.
# input layers
input_q1 <- layer_input(shape = max_len, name = "Q1")
input_q2 <- layer_input(shape = max_len, name = "Q2")

# embedding layers
q1_embeddings <- input_q1 %>% 
  layer_embedding(
    input_dim = vocab_size + 2,
    output_dim = embedding_size,
    input_length = max_len,
    name = "Q1_embeddings"
  )

q2_embeddings <- input_q2 %>% 
  layer_embedding(
    input_dim = vocab_size + 2,
    output_dim = embedding_size,
    input_length = max_len,
    name = "Q2_embeddings"
  )

# flatten embeddings
q1_em_flatten <- q1_embeddings %>% layer_flatten(name = "Q1_embeddings_flattened")
q2_em_flatten <- q2_embeddings %>% layer_flatten(name = "Q2_embeddings_flattened")

# dot product
dot <- layer_dot(list(q1_em_flatten, q2_em_flatten), axes = 1, name = "dot_product")

# output/prediction
pred <- dot %>% layer_dense(units = 1, activation = "sigmoid", name = "similarity_prediction")

Now that we have all the model components, we can create our functional keras model and establish the desired compiler.

embedding_model <- keras_model(list(input_q1, input_q2), pred)
embedding_model %>% compile(
  optimizer = "rmsprop", 
  loss = "binary_crossentropy", 
  metrics = "accuracy"
)

summary(embedding_model)
Model: "model_7"
_________________________________________________________________________________
Layer (type)              Output Shape      Param #   Connected to               
=================================================================================
Q1 (InputLayer)           [(None, 20)]      0                                    
_________________________________________________________________________________
Q2 (InputLayer)           [(None, 20)]      0                                    
_________________________________________________________________________________
Q1_embeddings (Embedding) (None, 20, 256)   2560512   Q1[0][0]                   
_________________________________________________________________________________
Q2_embeddings (Embedding) (None, 20, 256)   2560512   Q2[0][0]                   
_________________________________________________________________________________
Q1_embeddings_flattened ( (None, 5120)      0         Q1_embeddings[0][0]        
_________________________________________________________________________________
Q2_embeddings_flattened ( (None, 5120)      0         Q2_embeddings[0][0]        
_________________________________________________________________________________
dot_product (Dot)         (None, 1)         0         Q1_embeddings_flattened[0][
                                                      Q2_embeddings_flattened[0][
_________________________________________________________________________________
similarity_prediction (De (None, 1)         2         dot_product[0][0]          
=================================================================================
Total params: 5,121,026
Trainable params: 5,121,026
Non-trainable params: 0
_________________________________________________________________________________

Before we train our model, let’s create training and validation sets based on the sampling index used for the logistic regression benchmark model. That way we can compare results directly to the benchmark.

train_question1_padded <- question1_padded[index,]
train_question2_padded <- question2_padded[index,]
train_response <- quora_data$is_duplicate[index]

val_question1_padded <- question1_padded[-index,]
val_question2_padded <- question2_padded[-index,]
val_response <- quora_data$is_duplicate[-index]

Now we can train our model.

m1_history <- embedding_model %>%
  fit(
    list(train_question1_padded, train_question2_padded),
    train_response, 
    batch_size = 64, 
    epochs = 10, 
    validation_data = list(
      list(val_question1_padded, val_question2_padded), 
      val_response
    ),
    callbacks = list(
      callback_early_stopping(patience = 5, restore_best_weights = TRUE),
      callback_reduce_lr_on_plateau(patience = 3)
    )
  )

You should see an improvement over and above the benchmark model.

best_epoch <- which(m1_history$metrics$val_loss == min(m1_history$metrics$val_loss))
loss <- m1_history$metrics$val_loss[best_epoch] %>% round(3)
acc <- m1_history$metrics$val_accuracy[best_epoch] %>% round(3)

glue("The best epoch had a loss of {loss} and mean absolute error of {acc}")
The best epoch had a loss of 0.564 and mean absolute error of 0.717

Modeling with sequence embeddings using LSTMs

Let’s modify our model so that in addition to embedding our questions, we can use LSTMs to embed our sequences. We do this by adding an LSTM layer after the embedding layer as represented here:

The idea behind this is that the LSTM sequence embeddings may allow us to improve the ability to capture questions with strong similarities but worded in contrasting order and/or length. For example:

I am a 13-year-old boy that wants to learn how to program video games. What programming languages should I learn? How do I get started?

I am entering the world of video game programming and want to know what language I should learn? Because there are so many languages I do not know which one to start with. Can you recommend a language that’s easy to learn and can be used with many platforms?

To create this model structure, we can use the same input (input_q1 & input_q2) and word embedding (q1_embeddings & q2_embeddings) objects created earlier. However, since our word embeddings showed signs of overfitting, we can create new word embedding objects that include regularization (another hyperparameter that could be adjusted).

Now, instead of flattening the word embeddings, we will simply feed them into LSTM layers, which you can also add regularization if desired. Set the size of the LSTM layers to that specified with lstm_size (line 512). The output of an LSTM layer is a vector (1D tensor) so we do not need to flatten it prior to feeding it into the dot product, so the rest of the code is as it was earlier.

# word embeddings
q1_embeddings <- input_q1 %>% 
  layer_embedding(
    input_dim = vocab_size + 2,
    output_dim = embedding_size,
    input_length = max_len,
    embeddings_regularizer = regularizer_l2(0.0001),
    name = "Q1_embeddings"
  )

q2_embeddings <- input_q2 %>% 
  layer_embedding(
    input_dim = vocab_size + 2,
    output_dim = embedding_size,
    input_length = max_len,
    embeddings_regularizer = regularizer_l2(0.0001),
    name = "Q2_embeddings"
  )

# sequence embeddings
q1_lstm <- q1_embeddings %>%
  layer_lstm(
    units = lstm_size, 
    kernel_regularizer = regularizer_l2(0.0001), 
    name = "Q1_lstm"
    )

q2_lstm <- q2_embeddings %>%
  layer_lstm(
    units = lstm_size, 
    kernel_regularizer = regularizer_l2(0.0001),  
    name = "Q2_lstm"
    )

# dot product
dot <- layer_dot(list(q1_lstm, q2_lstm), axes = 1, name = "dot_product")

# output/prediction
pred <- dot %>% layer_dense(units = 1, activation = "sigmoid", name = "similarity_prediction")

Now create and compile the model as before.

model <- keras_model(list(input_q1, input_q2), pred)
model %>% compile(
  optimizer = "rmsprop", 
  loss = "binary_crossentropy", 
  metrics = "accuracy"
)

summary(model)
Model: "model_6"
_________________________________________________________________________________
Layer (type)              Output Shape      Param #   Connected to               
=================================================================================
Q1 (InputLayer)           [(None, 20)]      0                                    
_________________________________________________________________________________
Q2 (InputLayer)           [(None, 20)]      0                                    
_________________________________________________________________________________
Q1_embeddings (Embedding) (None, 20, 256)   2560512   Q1[0][0]                   
_________________________________________________________________________________
Q2_embeddings (Embedding) (None, 20, 256)   2560512   Q2[0][0]                   
_________________________________________________________________________________
Q1_lstm (LSTM)            (None, 512)       1574912   Q1_embeddings[0][0]        
_________________________________________________________________________________
Q2_lstm (LSTM)            (None, 512)       1574912   Q2_embeddings[0][0]        
_________________________________________________________________________________
dot_product (Dot)         (None, 1)         0         Q1_lstm[0][0]              
                                                      Q2_lstm[0][0]              
_________________________________________________________________________________
similarity_prediction (De (None, 1)         2         dot_product[0][0]          
=================================================================================
Total params: 8,270,850
Trainable params: 8,270,850
Non-trainable params: 0
_________________________________________________________________________________

Now train the model as before. Note how much slower training time is when we add LSTM layers!

m2_history <- lstm_model %>%
  fit(
    list(train_question1_padded, train_question2_padded),
    train_response, 
    batch_size = 64, 
    epochs = 20, 
    validation_data = list(
      list(val_question1_padded, val_question2_padded), 
      val_response
    ),
    callbacks = list(
      callback_early_stopping(patience = 5, restore_best_weights = TRUE),
      callback_reduce_lr_on_plateau(patience = 3)
    )
  )

You should see an improvement over and above the benchmark model.

best_epoch <- which(m2_history$metrics$val_loss == min(m2_history$metrics$val_loss))
loss <- m2_history$metrics$val_loss[best_epoch] %>% round(3)
acc <- m2_history$metrics$val_accuracy[best_epoch] %>% round(3)

glue("The best epoch had a loss of {loss} and mean absolute error of {acc}")

Next steps

Once you get the above models working on a smaller fraction of the data, go ahead and train them on the entire dataset (you’ll need to re-run the preprocessing steps). Feel free to tune the hyperparameters to find a near optimal model (although time will be a constraint). If you find the right model you will be able to get near 85% accuracy.

Although training time for these models can be quite slow, applying the models to new input (aka “scoring”) is quite fast. For example, the following function can be used to make predictions. In this function we preprocess the input data in the same way we preprocessed the training data (tokenizer).

predict_question_pairs <- function(model, tokenizer, q1, q2) {
  q1 <- texts_to_sequences(tokenizer, list(q1))
  q2 <- texts_to_sequences(tokenizer, list(q2))
  
  q1 <- pad_sequences(q1, 20)
  q2 <- pad_sequences(q2, 20)
  
  as.numeric(predict(model, list(q1, q2)))
}

We can now call it with new pairs of questions, for example:

Q1 <- "What's R programming?"
Q2 <- "What is R programming?"

pred <- predict_question_pairs(embedding_model, tokenizer, Q1, Q2)

glue("Our model suggests that the supplied questions have a {round(pred, 2)}",
     "probability of being duplicates.", .sep = " ")
Our model suggests that the supplied questions have a 0.69 probability of being duplicates.

Deploy the model

To demonstrate deployment of the trained model, I created a simple Shiny application, where you can paste 2 questions from Quora and find the probability of them being duplicates. Follow these steps to run the Shiny app:

  1. Save your final preprocessing tokenizer to the dl-keras-tf/materials/09-project directory.
save_text_tokenizer(tokenizer, "tokenizer-question-pairs")
  1. Save your final model to the dl-keras-tf/materials/09-project directory.
save_model_hdf5(model, "model-question-pairs.hdf5")
  1. Now launch the Shiny app by running the app-question-pairs.Rmd file in the dl-keras-tf/materials/09-project directory.

Speaking of duplication1


  1. This project is based on Daniel Falbel’s blog post “Classifying Duplicate Questions from Quora with Keras” on the TensorFlow for R blog, a great resource to learn from!

LS0tCnRpdGxlOiAiUHJvamVjdCAyOiBEZXRlY3RpbmcgRHVwbGljYXRlIFF1b3JhIFF1ZXN0aW9ucyIKb3V0cHV0OiBodG1sX25vdGVib29rCi0tLQoKYGBge3Igc2V0dXAsIGluY2x1ZGU9RkFMU0V9CmtuaXRyOjpvcHRzX2NodW5rJHNldCgKICBlY2hvID0gVFJVRSwKICBtZXNzYWdlID0gRkFMU0UsCiAgd2FybmluZyA9IEZBTFNFCiAgKQpgYGAKClRoaXMgcHJvamVjdCBpcyBkZXNpZ25lZCB0byB0ZXN0IHlvdXIgY3VycmVudCBrbm93bGVkZ2Ugb24gYXBwbHlpbmcgc2V2ZXJhbCBvZgp0aGUgc2tpbGxzIHlvdSBsZWFybmVkIHRvZGF5IChpLmUuIGVtYmVkZGluZ3MsIExTVE0sIGZ1bmN0aW9uYWwga2VyYXMgQVBJKS4gVGhlCm9iamVjdGl2ZSBpcyB0byBkZXZlbG9wIGEgbW9kZWwgdGhhdCBwcmVkaWN0cyB3aGljaCBvZiB0aGUgcHJvdmlkZWQgcGFpcnMgb2YKUXVvcmEgcXVlc3Rpb25zIGNvbnRhaW4gdGhlIHNhbWUgbWVhbmluZyAoY291bGQgYmUgY2xhc3NpZmllZCBhcyBkdXBsaWNhdGVzKS4KVGhlIGRhdGFzZXQgZmlyc3QgYXBwZWFyZWQgaW4gdGhlIEthZ2dsZSBjb21wZXRpdGlvbgpbUXVvcmEgUXVlc3Rpb24gUGFpcnNdKGh0dHBzOi8vd3d3LmthZ2dsZS5jb20vYy9xdW9yYS1xdWVzdGlvbi1wYWlycykgYW5kCmNvbnNpc3RzIG9mIGFwcHJveGltYXRlbHkgNDAwLDAwMCBwYWlycyBvZiBxdWVzdGlvbnMgYWxvbmcgd2l0aCBhIGNvbHVtbgppbmRpY2F0aW5nIGlmIHRoZSBxdWVzdGlvbiBwYWlyIGlzIGNvbnNpZGVyZWQgYSBkdXBsaWNhdGUuCgpBZnRlciB5b3UgY29tcGxldGUgdGhpcyBwcm9qZWN0LCB5b3UgY2FuIHJlYWQgYWJvdXQgUXVvcmEncyBhcHByb2FjaCB0byB0aGlzCnByb2JsZW0gaW4gdGhpcyBbYmxvZyBwb3N0XShodHRwczovL2JpdC5seS8zOXBvOFZJKS4KCl9fX0dvb2QgbHVjayFfX18KCiMgUGFja2FnZSBSZXF1aXJlbWVudHMKCmBgYHtyfQpsaWJyYXJ5KGtlcmFzKQpsaWJyYXJ5KHRpZHl2ZXJzZSkKbGlicmFyeShyc2FtcGxlKQpsaWJyYXJ5KHRlc3R0aGF0KQpsaWJyYXJ5KGdsdWUpCmBgYAoKIyBJbXBvcnQgRGF0YQoKRGF0YSBjYW4gYmUgZG93bmxvYWRlZCBmcm9tIHRoZSBLYWdnbGUgZGF0YXNldCB3ZWJwYWdlIG9yIGZyb20gUXVvcmHigJlzIHJlbGVhc2UKb2YgdGhlIGRhdGFzZXQuIFRoZSBkYXRhIHNldCBzaG91bGQgY29udGFpbiA0MDQsMjkwIG9ic2VydmF0aW9ucyBhbmQgNiBjb2x1bW5zLgoKYGBge3J9CnF1b3JhX2RhdGEgPC0gZ2V0X2ZpbGUoCiAgInF1b3JhX2R1cGxpY2F0ZV9xdWVzdGlvbnMudHN2IiwKICAiaHR0cDovL3FpbS5mcy5xdW9yYWNkbi5uZXQvcXVvcmFfZHVwbGljYXRlX3F1ZXN0aW9ucy50c3YiCikgJT4lCiAgcmVhZF90c3YoKQoKZXhwZWN0X2VxdWFsKGRpbShxdW9yYV9kYXRhKSwgYyg0MDQyOTAsIDYpKQpgYGAKCjxicj48Y2VudGVyPuKaoO+4j+KaoO+4j+KaoO+4j+KaoO+4j+KaoO+4j+KaoO+4j+KaoO+4j+KaoO+4j+KaoO+4j+KaoO+4j+KaoO+4j+KaoO+4j+KaoO+4j+KaoO+4j++4j+KaoO+4j+KaoO+4j+KaoO+4j+KaoO+4j+KaoO+4j+KaoO+4jzwvY2VudGVyPjxicj4KCk9mdGVuIHdoZW4geW91IGFyZSB3b3JraW5nIHdpdGggbGFyZ2UgZGF0YXNldHMsIGl0IGlzIHdpc2UgdG8ganVzdCBzdGFydCB3aXRoCmEgZnJhY3Rpb24gb2YgdGhlIGRhdGEgdG8gZG8gaW5pdGlhbCBtb2RlbCBleHBsb3JhdGlvbi4gT25jZSB5b3UgaGF2ZSBhIGdvb2QKd29ya2luZyBtb2RlbCwgdGhlbiB5b3UgY2FuIHJlbW92ZSB0aGlzIGNvZGUgY2h1bmsgYW5kIHJ1biB0aGUgYW5hbHlzaXMgb24gdGhlCmZ1bGwgZGF0YXNldC4gTm90ZSB0aGF0IG1vZGVsaW5nIHdpdGggdGhlIGZ1bGwgZGF0YXNldCBjYW4gdGFrZSBtdWx0aXBsZSBob3VycwpvbiBhIENQVSBhbmQgZXZlbiBjbG9zZSB0byBhbiBob3VyIG9uIGEgR1BVIHdoZW4gdXNpbmcgTFNUTSBsYXllcnMuCgo8YnI+PGNlbnRlcj7imqDvuI/imqDvuI/imqDvuI/imqDvuI/imqDvuI/imqDvuI/imqDvuI/imqDvuI/imqDvuI/imqDvuI/imqDvuI/imqDvuI/imqDvuI/imqDvuI/vuI/imqDvuI/imqDvuI/imqDvuI/imqDvuI/imqDvuI/imqDvuI88L2NlbnRlcj4KCmBgYHtyfQojIHJlbW92ZSBhZnRlciB5b3UgaGF2ZSBhIGdvb2Qgd29ya2luZyBtb2RlbApzZXQuc2VlZCgxMjMpCnF1b3JhX2RhdGEgPC0gcXVvcmFfZGF0YSAlPiUgc2FtcGxlX2ZyYWMoMC4wNSkKZGltKHF1b3JhX2RhdGEpCmBgYAoKT3VyIGRhdGFzZXQgY29udGFpbnMgc2l4IGNvbHVtbnMuIFRoZSBsYXN0IHRocmVlIGNvbHVtbnMgYXJlIHRoZSBvbmVzIG9mCmludGVyZXN0OgoKKiBfX2lkX186IG9ic2VydmF0aW9uIChha2Egcm93KSBJRAoqIF9fcWlkMV9fLCBfX3FpZDJfXzogdW5pcXVlIElEcyBvZiBlYWNoIHF1ZXN0aW9uCiogX19xdWVzdGlvbjFfXywgX19xdWVzdGlvbjJfXzogdGhlIGZ1bGwgdGV4dCBvZiBlYWNoIHF1ZXN0aW9uCiogX19pc19kdXBsaWNhdGVfXzogdGhlIHRhcmdldCB2YXJpYWJsZSwgc2V0IHRvIDEgaWYgcXVlc3Rpb24xIGFuZCBxdWVzdGlvbjIKICBoYXZlIGVzc2VudGlhbGx5IHRoZSBzYW1lIG1lYW5pbmcsIGFuZCAwIG90aGVyd2lzZS4KCmBgYHtyfQpxdW9yYV9kYXRhCmBgYAoKIyBEYXRhIEV4cGxvcmF0aW9uCgpXZSBjYW4gc2VlIHRoYXQgYXBwcm94aW1hdGVseSAzNiUgb2Ygb3VyIHF1ZXN0aW9uIHBhaXJzIHJlcHJlc2VudCBxdWVzdGlvbnMgd2l0aApzaW1pbGFyIG1lYW5pbmcgKGFrYSBkdXBsaWNhdGVzKS4gRm9yIHRoaXMgcHJvamVjdCB5b3UgZG8gbm90IG5lZWQgdG8gd29ycnkKYWJvdXQgcmVzYW1wbGluZyBwcm9jZWR1cmVzIHRvIGJhbGFuY2UgdGhlIHJlc3BvbnNlLgoKYGBge3J9CnRhYmxlKHF1b3JhX2RhdGEkaXNfZHVwbGljYXRlKSAlPiUgcHJvcC50YWJsZSgpCmBgYAoKTm93IGxldCdzIGFzc2VzcyBvdXIgcXVlc3Rpb25zIHRleHQuIEZpcnN0LCB3ZSdsbCBnZXQgYWxsIHVuaXF1ZSBxdWVzdGlvbnMuCgpgYGB7cn0KdW5pcXVlX3F1ZXN0aW9ucyA8LSB1bmlxdWUoYyhxdW9yYV9kYXRhJHF1ZXN0aW9uMSwgcXVvcmFfZGF0YSRxdWVzdGlvbjIpKQpgYGAKCkxldOKAmXMgdGFrZSBhIGxvb2sgYXQgdGhlIG51bWJlciBvZiB1bmlxdWUgd29yZHMgYWNyb3NzIGFsbCBvdXIgcXVlc3Rpb25zLiBUaGlzCndpbGwgaGVscHMgdXMgdG8gZGVjaWRlIHRoZSB2b2NhYnVsYXIgc2l6ZSwgd2hpY2ggY2FuIGJlIHRyZWF0ZWQgYXMgYQpoeXBlcnBhcmFtZXRlciBvZiBvdXIgbW9kZWwuCgpUaGUgZm9sbG93aW5nIGNvZGUgY2h1bms6CgoxLiBzcGxpdHMgdXAgYWxsIG91ciB0ZXh0IGludG8gaW5kaXZpZHVhbCB3b3JkcywKMi4gZG9lcyBzb21lIHRleHQgY2xlYW5pbmcgdG8gcmVtb3ZlIHB1bmN0dWF0aW9ucyBhbmQgbm9ybWFsaXplIHRoZSBjYXNlLAozLiBhbmQgY29tcHV0ZXMgdGhlIG51bWJlciBvZiB1bmlxdWUgd29yZHMuCgpXZSBjYW4gc2VlIHRoYXQgdGhlcmUgYXJlIDExMCw4NDIgdW5pcXVlIHdvcmRzIGluIHRoZSBmdWxsIGRhdGFzZXQgKDIzLDczOQp1bmlxdWUgd29yZHMgd2hlbiB1c2luZyBvdXIgc2FtcGxlZCBkYXRhKS4KCmBgYHtyfQp1bmlxdWVfcXVlc3Rpb25zICU+JSAKICBzdHJfc3BsaXQocGF0dGVybiA9ICIgIikgJT4lIAogIHVubGlzdCgpICU+JSAKICBzdHJfcmVtb3ZlX2FsbChwYXR0ZXJuID0gIltbOnB1bmN0Ol1dIikgJT4lCiAgc3RyX3RvX2xvd2VyKCkgJT4lCiAgdW5pcXVlKCkgJT4lIAogIGxlbmd0aCgpCmBgYAoKTGV04oCZcyB0YWtlIGEgbG9vayBhdCB0aGUgbnVtYmVyIG9mIHdvcmRzIGluIGVhY2ggcXVlc3Rpb24uIFRoaXMgd2lsbCBoZWxwcyB1cyB0bwpkZWNpZGUgdGhlIHBhZGRpbmcgbGVuZ3RoLCBhbm90aGVyIGh5cGVycGFyYW1ldGVyIG9mIG91ciBtb2RlbC4gV2UgY2FuIHNlZSB0aGF0Cm5lYXJseSBhbGwgcXVlc3Rpb25zIGhhdmUgMzIgb3IgbGVzcyB3b3Jkcy4KCmBgYHtyfQp1bmlxdWVfcXVlc3Rpb25zICU+JQogIG1hcF9pbnQofiBzdHJfY291bnQoLiwgIlxcdysiKSkgJT4lCiAgcXVhbnRpbGUoYygwLjI1LCAwLjUsIDAuNzUsIDAuOTkpLCBuYS5ybSA9IFRSVUUpCmBgYAoKIyBTZXQgYmFzaWMgaHlwZXJwYXJhbWV0ZXJzCgpMZXQncyBnbyBhaGVhZCBhbmQgc2V0IHNvbWUgYmFzaWMgaHlwZXJwYXJhbWV0ZXJzIGZvciBvdXIgbW9kZWwuIFRoZXNlIGFyZSBhbGwKdmFsdWVzIHRoYXQgeW91IGNhbiBhZGp1c3QgaWYgdGltZSBhbGxvd3MuCgoqIF9fdm9jYWJfc2l6ZV9fOiBUaGUgc2l6ZSBvZiBvdXIgdm9jYWIuIE9mdGVuLCB3ZSBzdGFydCB3aXRoIGFib3V0IDUwJSBvZiBvdXIKICB0b3RhbCB2b2NhYiBzaXplLiBTbyB3ZSBjYW4gc3RhcnQgd2l0aCBhYm91dCAxMCwwMDAgZm9yIG91ciBzYW1wbGVkIGRhdGFzZXQKICBhbmQgNTAsMDAwIGZvciB0aGUgZW50aXJlIGRhdGFzZXQuIFRoaXMgd291bGQgYmUgYSBoeXBlcnBhcmFtZXRlciB5b3UgdHVuZQogIGZvciBvcHRpbWFsIHBlcmZvcm1hbmNlLgoqIF9fbWF4X2xlbl9fOiBMZW5ndGggdG8gcGFkIGVhY2ggcXVlc3Rpb24gdG8uIFNpbmNlIHRoZSB0ZXh0IGlzIHNob3J0ZXIsIGFuZAogIHdlIHdhbnQgdG8gY2FwdHVyZSBhcyBtdWNoIGNvbnRlbnQgYXMgcG9zc2libGUgaW4gZWFjaCBxdWVzdGlvbiwgd2UgY2FuIHNldAogIHRoaXMgdXNpbmcgdGhlIHVwcGVyIHF1YW50aWxlICg4MC05NSUpIG9mIHRoZSB3b3JkIGRpc3RyaWJ1dGlvbiwgd2hpY2ggZXF1YXRlcwogIHRvIDE1LTI1LgoqIF9fZW1iZWRkaW5nX3NpemVfXzogVGhlIHNpemUgb2YgdGhlIHdvcmQgZW1iZWRkaW5ncy4gV2UnbGwgc3RhcnQgbGFyZ2Ugc2luY2UKICB3ZSB3YW50IHRvIGNhcHR1cmUgZmluZSByZWxhdGlvbnNoaXBzIGFyb3NzIHNlbWFudGljIG1lYW5pbmdzLgoqIF9fbHN0bV9zaXplX186IFRoZSBzaXplIG9mIHRoZSBMU1RNIHNlcXVlbmNlIGVtYmVkZGluZy4gU2ltaWxhciB0byB0aGUgd29yZAogIGVtYmVkZGluZyBzaXplLCBXZSdsbCBzdGFydCBsYXJnZSBzaW5jZSB3ZSB3YW50IHRvIGNhcHR1cmUgZmluZSByZWxhdGlvbnNoaXBzCiAgYXJvc3Mgc2VtYW50aWMgbWVhbmluZ3MuCgpgYGB7cn0Kdm9jYWJfc2l6ZSA8LSAxMDAwMAptYXhfbGVuIDwtIDIwCmVtYmVkZGluZ19zaXplIDwtIDI1Ngpsc3RtX3NpemUgPC0gNTEyCmBgYAoKIyBQcmVwcm9jZXNzIG91ciB0ZXh0CgpOZXh0LCBsZXQncyBjcmVhdGUgYSB0ZXh0IHRva2VuaXplciB0byBkZWZpbmUgaG93IHdlIHdhbnQgdG8gcHJlcHJvY2VzcyB0aGUgdGV4dAooaS5lLiBjb252ZXJ0IHRvIGxvd2VyY2FzZSwgcmVtb3ZlIHB1bmN0dWF0aW9uLCB0b2tlbiBzcGxpdHRpbmcgY2hhcmFjdGVycykgYW5kCnRoZW4gYXBwbHkgaXQgdG8gb3VyIHF1ZXN0aW9uIHRleHQuCgpgYGB7cn0KdG9rZW5pemVyIDwtIHRleHRfdG9rZW5pemVyKG51bV93b3JkcyA9IHZvY2FiX3NpemUpICU+JQogIGZpdF90ZXh0X3Rva2VuaXplcih1bmlxdWVfcXVlc3Rpb25zKQpgYGAKCk5leHQsIGxldCdzIGNyZWF0ZSB0d28gbmV3IG9iamVjdHM6CgoqIGBxdWVzdGlvbjFgOiB0aGUgdGV4dCB0b2tlbml6ZXIgZm9yIGFsbCBgcXVvcmFfZGF0YSRxdWVzdGlvbjFgIHRleHQKKiBgcXVlc3Rpb24yYDogdGhlIHRleHQgdG9rZW5pemVyIGZvciBhbGwgYHF1b3JhX2RhdGEkcXVlc3Rpb24yYCB0ZXh0CgpgYGB7cn0KcXVlc3Rpb24xIDwtIHRleHRzX3RvX3NlcXVlbmNlcyh0b2tlbml6ZXIsIHF1b3JhX2RhdGEkcXVlc3Rpb24xKQpxdWVzdGlvbjIgPC0gdGV4dHNfdG9fc2VxdWVuY2VzKHRva2VuaXplciwgcXVvcmFfZGF0YSRxdWVzdGlvbjIpCmBgYAoKSWYgeW91IGxvb2sgYXQgdGhlIGZpcnN0IDYgYHF1ZXN0aW9uMWAgb2JzLCB3ZSBzZWUgdGhhdCB0aGV5IGFyZSBvZiBkaWZmZXJlbnQKbGVuZ3RoLiBUbyBjcmVhdGUgZW1iZWRkaW5ncyBmb3IgdGhlc2UgcXVlc3Rpb25zLCB3ZSBuZWVkIHRvIHN0YW5kYXJkaXplIHRoZWlyCmxlbmd0aC4gIEdvIGFoZWFkIGFuZCBjcmVhdGUgdHdvIG5ldyBvYmplY3RzIChgcXVlc3Rpb24xX3BhZGRlZGAgJiBgcXVlc3Rpb24yX3BhZGRlZGApCnRoYXQgcGFkIGBxdWVzdGlvbjFgIGFuZCBgcXVlc3Rpb24yYC4gCgpUaGUgZGVmYXVsdCBwYWRkaW5nIHZhbHVlIGlzIDAsIGJ1dCB0aGlzIHZhbHVlIGlzIG9mdGVuIHVzZWQgZm9yIHdvcmRzIHRoYXQKZG9u4oCZdCBhcHBlYXIgd2l0aGluIHRoZSBlc3RhYmxpc2hlZCB3b3JkIGluZGV4LiBXZSBjb3VsZCBzdGlsbCBwYWQgd2l0aCB0aGlzCnZhbHVlIG9yIHdlIGNvdWxkIGRpZmZlcmVudGlhdGUgdGhpcyB2YWx1ZSBieSBwYWRkaW5nIHdpdGggYHZvY2FiX3NpemUgKyAxYC4KCmBgYHtyfQpxdWVzdGlvbjFfcGFkZGVkIDwtIHBhZF9zZXF1ZW5jZXMocXVlc3Rpb24xLCBtYXhsZW4gPSBtYXhfbGVuLCB2YWx1ZSA9IHZvY2FiX3NpemUgKyAxKQpxdWVzdGlvbjJfcGFkZGVkIDwtIHBhZF9zZXF1ZW5jZXMocXVlc3Rpb24yLCBtYXhsZW4gPSBtYXhfbGVuLCB2YWx1ZSA9IHZvY2FiX3NpemUgKyAxKQpgYGAKCldlIGhhdmUgbm93IGZpbmlzaGVkIHRoZSBwcmVwcm9jZXNzaW5nIHN0ZXBzLiBXZSB3aWxsIG5vdyBydW4gYSBzaW1wbGUgYmVuY2htYXJrCm1vZGVsIGJlZm9yZSBtb3Zpbmcgb24gdG8gdGhlIEtlcmFzIG1vZGVsLgoKIyBTaW1wbGUgYmVuY2htYXJrCgpCZWZvcmUgY3JlYXRpbmcgYSBjb21wbGljYXRlZCBtb2RlbCBsZXTigJlzIHRha2UgYSBzaW1wbGUgYXBwcm9hY2guIExldOKAmXMgY3JlYXRlCnR3byBwcmVkaWN0b3JzOiBwZXJjZW50YWdlIG9mIHdvcmRzIGZyb20gcXVlc3Rpb24xIHRoYXQgYXBwZWFyIGluIHRoZSBxdWVzdGlvbjIKYW5kIHZpY2UtdmVyc2EuIFRoZW4gd2Ugd2lsbCB1c2UgYSBsb2dpc3RpYyByZWdyZXNzaW9uIHRvIHByZWRpY3QgaWYgdGhlCnF1ZXN0aW9ucyBhcmUgZHVwbGljYXRlcy4KCmBgYHtyfQpwZXJjX3dvcmRzX3F1ZXN0aW9uMSA8LSBtYXAyX2RibChxdWVzdGlvbjEsIHF1ZXN0aW9uMiwgfm1lYW4oLnggJWluJSAueSkpCnBlcmNfd29yZHNfcXVlc3Rpb24yIDwtIG1hcDJfZGJsKHF1ZXN0aW9uMiwgcXVlc3Rpb24xLCB+IG1lYW4oLnggJWluJSAueSkpCgpkZl9tb2RlbCA8LSBkYXRhLmZyYW1lKAogIHBlcmNfd29yZHNfcXVlc3Rpb24xID0gcGVyY193b3Jkc19xdWVzdGlvbjEsCiAgcGVyY193b3Jkc19xdWVzdGlvbjIgPSBwZXJjX3dvcmRzX3F1ZXN0aW9uMiwKICBpc19kdXBsaWNhdGUgPSBxdW9yYV9kYXRhJGlzX2R1cGxpY2F0ZQopICU+JQogIG5hLm9taXQoKQpgYGAKCk5vdyB0aGF0IHdlIGhhdmUgb3VyIHByZWRpY3RvcnMsIGxldOKAmXMgY3JlYXRlIHRoZSBsb2dpc3RpYyBtb2RlbC4gV2Ugd2lsbCB0YWtlIGEKc21hbGwgc2FtcGxlIGZvciB2YWxpZGF0aW9uLgoKYGBge3J9CnNldC5zZWVkKDEyMykKaW5kZXggPC0gc2FtcGxlLmludChucm93KGRmX21vZGVsKSwgMC45ICogbnJvdyhkZl9tb2RlbCkpCmJlbmNobWFya190cmFpbiA8LSBkZl9tb2RlbFtpbmRleCwgXQpiZW5jaG1hcmtfdmFsaWQgPC0gZGZfbW9kZWxbLWluZGV4LCBdCgpsb2dpc3RpY19yZWdyZXNzaW9uIDwtIGdsbSgKICBpc19kdXBsaWNhdGUgfiBwZXJjX3dvcmRzX3F1ZXN0aW9uMSArIHBlcmNfd29yZHNfcXVlc3Rpb24yLCAKICBmYW1pbHkgPSAiYmlub21pYWwiLAogIGRhdGEgPSBiZW5jaG1hcmtfdHJhaW4KKQoKc3VtbWFyeShsb2dpc3RpY19yZWdyZXNzaW9uKQpgYGAKCkxldOKAmXMgY2FsY3VsYXRlIHRoZSBhY2N1cmFjeSBvbiBvdXIgdmFsaWRhdGlvbiBzZXQuCgpgYGB7cn0KcHJlZCA8LSBwcmVkaWN0KGxvZ2lzdGljX3JlZ3Jlc3Npb24sIGJlbmNobWFya192YWxpZCwgdHlwZSA9ICJyZXNwb25zZSIpCnByZWQgPC0gcHJlZCA+IG1lYW4oYmVuY2htYXJrX3ZhbGlkJGlzX2R1cGxpY2F0ZSkKYWNjdXJhY3kgPC0gdGFibGUocHJlZCwgYmVuY2htYXJrX3ZhbGlkJGlzX2R1cGxpY2F0ZSkgJT4lIAogIHByb3AudGFibGUoKSAlPiUgCiAgZGlhZygpICU+JSAKICBzdW0oKQoKZ2x1ZSgiT3VyIGJlbmNobWFyayBtb2RlbCBhY2hpZXZlcyBhIHtyb3VuZChhY2N1cmFjeSAqIDEwMCwgMil9JSBhY2N1cmFjeS4iKQpgYGAKCk5vdyB5b3VyIGdvYWwgaXMgdG8gY3JlYXRlIGEga2VyYXMgbW9kZWwgdGhhdCBvdXQgcGVyZm9ybXMgdGhpcyBiZW5jaG1hcmsuCgojIE1vZGVsaW5nIHdpdGgganVzdCBlbWJlZGRpbmdzCgpGb3Igb3VyIGZpcnN0IG1vZGVsLCB3ZSdsbCB1c2UgYSBzaW1pbGFyIGFyY2hpdGVjdHVyZSBhcyB3ZSBzYXcgaW4gdGhlCmNvbGxhYm9yYXRpdmUgZmlsdGVyaW5nIG5vdGVib29rIFvihLnvuI9dKGh0dHA6Ly9iaXQubHkvZGwtY2Ytbm90ZWJvb2spIHdpdGggYQpjb3VwbGUgb2YgYWRqdXN0bWVudHMuIEluIGVzc2VuY2UsIHdlIHdhbnQgdG8gYnVpbGQgYSBtb2RlbCB0aGF0OgoKMS4gY3JlYXRlcyB3b3JkIGVtYmVkZGluZ3MgZm9yIHF1ZXN0aW9uIDEgYW5kIDIsCjIuIGNvbXB1dGVzIHRoZSBkb3QgcHJvZHVjdCBvZiB0aGVzZSBlbWJlZGRpbmdzLAozLiBhZGRzIGEgZGVuc2UgY2xhc3NpZmllciB0byBwcmVkaWN0IGlmIHRoZSBxdWVzdGlvbnMgYXJlIGR1cGxpY2F0ZXMuCgpgYGB7ciwgZWNobz1GQUxTRX0Ka25pdHI6OmluY2x1ZGVfZ3JhcGhpY3MoInAyLW1vZGVsMS1zdHJ1Y3R1cmUucG5nIikKYGBgCgpGaXJzdCwgY3JlYXRlIHRoZSBrZXJhcyBtb2RlbCBjb21wb25lbnRzIGJ5IGZpbGxpbmcgaW4gdGhlIGJsYW5rcy4KCjEuIElucHV0IGxheWVycwogICAtIHdlIG5lZWQgdHdvIGlucHV0cywgb25lIGZvciBlYWNoIHF1ZXN0aW9uCiAgIC0gdGhlIGlucHV0IGxheWVyIHNoYXBlIG5lZWRzIHRvIGJlIHRoZSBzYW1lIHNpemUgYXMgdGhlIG51bWJlciBvZiBjb2x1bW5zIG9mCiAgICAgb3VyIGlucHV0cyAoYHF1ZXN0aW9uMV9wYWRkZWRgLCBgcXVlc3Rpb24yX3BhZGRlZGApCjIuIEVtYmVkZGluZyBsYXllcnMKICAgLSBidWlsZCBvbnRvIG91ciBpbnB1dCBsYXllcnMKICAgLSBgaW5wdXRfZGltYDogaW4gdGhpcyBjYXNlIGBpbnB1dF9kaW1gIGVxdWFscyBgdm9jYWJfc2l6ZSArIDJgIHNpbmNlIHdlIGhhdmUKICAgICAgdGhlIG51bWJlciBvZiB3b3JkcyAoYHZvY2FiX3NpemVgKSwgcGx1cyB0aGUgdmFsdWUgMCByZXByZXNlbnRzIHdvcmRzIG5vdAogICAgICBpbiBvdXIgZGVjbGFyZWQgZnJlcXVlbmN5LCBhbmQgdGhlIHZhbHVlIGB2b2NhYl9zaXplICsgMWAgcmVwcmVzZW50cyBvdXIKICAgICAgcGFkZGluZyB2YWx1ZS4KICAgLSBgb3V0cHV0X2RpbWA6IHJlcHJlc2VudHMgdGhlIGRlc2lyZWQgZW1iZWRkaW5ncyBkaW1lbnNpb24gKGxpbmUgMTU0KS4KICAgLSBgaW5wdXRfbGVuZ3RoYDogcmVwcmVzZW50cyB0aGUgbGVuZ3RoIG9mIG91ciBpbnB1dHMgKGBtYXhfbGVuYCkuIFdlIG5lZWQKICAgICAgdGhpcyB2YWx1ZSBiZWNhdXNlIHdlIG5lZWQgdG8gZmxhdHRlbiBvdXIgZW1iZWRkaW5ncyBmb3IgZG93bnN0cmVhbSBjb21wdXRhdGlvbi4KMy4gRmxhdHRlbiBsYXllcgogICAtIEluIHRoZSBjb2xsYWJvcmF0aXZlIGZpbHRlcmluZyBub3RlYm9vaywgd2Ugd2VyZSBlbWJlZGRpbmcgc2luZ2xlIGlucHV0CiAgICAgdmFsdWVzIChpLmUuIHVzZXIgSUQgJiBtb3ZpZSBJRCkuIEluIHRoaXMgZXhhbXBsZSwgd2UgYXJlIGVtYmVkZGluZyBpbnB1dHMKICAgICBvZiBsZW5ndGggMjAgKGBtYXhfbGVuYCksIHdoaWNoIHJlc3VsdHMgaW4gYSBtYXRyaXguIENvbnNlcXVlbnRseSwgd2UKICAgICBmbGF0dGVuIHRoZXNlIGVtYmVkZGluZyBtYXRyaWNlcyBzbyB0aGF0IHdlIGNhbiBjb21wdXRlIHRoZSBkb3QgcHJvZHVjdC4KNC4gRG90IHByb2R1Y3QKICAgLSBUaGUgZG90IHByb2R1Y3Qgd2lsbCBiZSBjb21wdXRlZCBmb3Igb3VyIGZsYXR0ZW5lZCBlbWJlZGRpbmdzIG9mIHF1ZXN0aW9uIDEKICAgICBhbmQgMi4KICAgLSBTaW5jZSB3ZSBmbGF0dGVuZWQgb3VyIGVtYmVkZGluZ3Mgd2UgdXNlIGBheGVzID0gMWAKNS4gT3V0cHV0L3ByZWRpY3Rpb24KICAgLSBTaW5jZSBvdXIgcmVzcG9uc2UgaXMgYmluYXJ5ICgxID0gaXMgZHVwbGljYXRlLCAwID0gbm90IGR1cGxpY2F0ZSksIHdlIG5lZWQKICAgICB0byB1c2UgdGhlIGFwcGxpY2FibGUgYWN0aXZhdGlvbiBmdW5jdGlvbi4KICAgICAKCmBgYHtyfQojIGlucHV0IGxheWVycwppbnB1dF9xMSA8LSBsYXllcl9pbnB1dChzaGFwZSA9IG1heF9sZW4sIG5hbWUgPSAiUTEiKQppbnB1dF9xMiA8LSBsYXllcl9pbnB1dChzaGFwZSA9IG1heF9sZW4sIG5hbWUgPSAiUTIiKQoKIyBlbWJlZGRpbmcgbGF5ZXJzCnExX2VtYmVkZGluZ3MgPC0gaW5wdXRfcTEgJT4lIAogIGxheWVyX2VtYmVkZGluZygKICAgIGlucHV0X2RpbSA9IHZvY2FiX3NpemUgKyAyLAogICAgb3V0cHV0X2RpbSA9IGVtYmVkZGluZ19zaXplLAogICAgaW5wdXRfbGVuZ3RoID0gbWF4X2xlbiwKICAgIG5hbWUgPSAiUTFfZW1iZWRkaW5ncyIKICApCgpxMl9lbWJlZGRpbmdzIDwtIGlucHV0X3EyICU+JSAKICBsYXllcl9lbWJlZGRpbmcoCiAgICBpbnB1dF9kaW0gPSB2b2NhYl9zaXplICsgMiwKICAgIG91dHB1dF9kaW0gPSBlbWJlZGRpbmdfc2l6ZSwKICAgIGlucHV0X2xlbmd0aCA9IG1heF9sZW4sCiAgICBuYW1lID0gIlEyX2VtYmVkZGluZ3MiCiAgKQoKIyBmbGF0dGVuIGVtYmVkZGluZ3MKcTFfZW1fZmxhdHRlbiA8LSBxMV9lbWJlZGRpbmdzICU+JSBsYXllcl9mbGF0dGVuKG5hbWUgPSAiUTFfZW1iZWRkaW5nc19mbGF0dGVuZWQiKQpxMl9lbV9mbGF0dGVuIDwtIHEyX2VtYmVkZGluZ3MgJT4lIGxheWVyX2ZsYXR0ZW4obmFtZSA9ICJRMl9lbWJlZGRpbmdzX2ZsYXR0ZW5lZCIpCgojIGRvdCBwcm9kdWN0CmRvdCA8LSBsYXllcl9kb3QobGlzdChxMV9lbV9mbGF0dGVuLCBxMl9lbV9mbGF0dGVuKSwgYXhlcyA9IDEsIG5hbWUgPSAiZG90X3Byb2R1Y3QiKQoKIyBvdXRwdXQvcHJlZGljdGlvbgpwcmVkIDwtIGRvdCAlPiUgbGF5ZXJfZGVuc2UodW5pdHMgPSAxLCBhY3RpdmF0aW9uID0gInNpZ21vaWQiLCBuYW1lID0gInNpbWlsYXJpdHlfcHJlZGljdGlvbiIpCmBgYAoKTm93IHRoYXQgd2UgaGF2ZSBhbGwgdGhlIG1vZGVsIGNvbXBvbmVudHMsIHdlIGNhbiBjcmVhdGUgb3VyIGZ1bmN0aW9uYWwga2VyYXMKbW9kZWwgYW5kIGVzdGFibGlzaCB0aGUgZGVzaXJlZCBjb21waWxlci4KCmBgYHtyfQplbWJlZGRpbmdfbW9kZWwgPC0ga2VyYXNfbW9kZWwobGlzdChpbnB1dF9xMSwgaW5wdXRfcTIpLCBwcmVkKQplbWJlZGRpbmdfbW9kZWwgJT4lIGNvbXBpbGUoCiAgb3B0aW1pemVyID0gInJtc3Byb3AiLCAKICBsb3NzID0gImJpbmFyeV9jcm9zc2VudHJvcHkiLCAKICBtZXRyaWNzID0gImFjY3VyYWN5IgopCgpzdW1tYXJ5KGVtYmVkZGluZ19tb2RlbCkKYGBgCgpCZWZvcmUgd2UgdHJhaW4gb3VyIG1vZGVsLCBsZXQncyBjcmVhdGUgdHJhaW5pbmcgYW5kIHZhbGlkYXRpb24gc2V0cyBiYXNlZCBvbgp0aGUgc2FtcGxpbmcgaW5kZXggdXNlZCBmb3IgdGhlIGxvZ2lzdGljIHJlZ3Jlc3Npb24gYmVuY2htYXJrIG1vZGVsLiBUaGF0IHdheSB3ZQpjYW4gY29tcGFyZSByZXN1bHRzIGRpcmVjdGx5IHRvIHRoZSBiZW5jaG1hcmsuCgpgYGB7cn0KdHJhaW5fcXVlc3Rpb24xX3BhZGRlZCA8LSBxdWVzdGlvbjFfcGFkZGVkW2luZGV4LF0KdHJhaW5fcXVlc3Rpb24yX3BhZGRlZCA8LSBxdWVzdGlvbjJfcGFkZGVkW2luZGV4LF0KdHJhaW5fcmVzcG9uc2UgPC0gcXVvcmFfZGF0YSRpc19kdXBsaWNhdGVbaW5kZXhdCgp2YWxfcXVlc3Rpb24xX3BhZGRlZCA8LSBxdWVzdGlvbjFfcGFkZGVkWy1pbmRleCxdCnZhbF9xdWVzdGlvbjJfcGFkZGVkIDwtIHF1ZXN0aW9uMl9wYWRkZWRbLWluZGV4LF0KdmFsX3Jlc3BvbnNlIDwtIHF1b3JhX2RhdGEkaXNfZHVwbGljYXRlWy1pbmRleF0KYGBgCgpOb3cgd2UgY2FuIHRyYWluIG91ciBtb2RlbC4KCmBgYHtyfQptMV9oaXN0b3J5IDwtIGVtYmVkZGluZ19tb2RlbCAlPiUKICBmaXQoCiAgICBsaXN0KHRyYWluX3F1ZXN0aW9uMV9wYWRkZWQsIHRyYWluX3F1ZXN0aW9uMl9wYWRkZWQpLAogICAgdHJhaW5fcmVzcG9uc2UsIAogICAgYmF0Y2hfc2l6ZSA9IDY0LCAKICAgIGVwb2NocyA9IDEwLCAKICAgIHZhbGlkYXRpb25fZGF0YSA9IGxpc3QoCiAgICAgIGxpc3QodmFsX3F1ZXN0aW9uMV9wYWRkZWQsIHZhbF9xdWVzdGlvbjJfcGFkZGVkKSwgCiAgICAgIHZhbF9yZXNwb25zZQogICAgKSwKICAgIGNhbGxiYWNrcyA9IGxpc3QoCiAgICAgIGNhbGxiYWNrX2Vhcmx5X3N0b3BwaW5nKHBhdGllbmNlID0gNSwgcmVzdG9yZV9iZXN0X3dlaWdodHMgPSBUUlVFKSwKICAgICAgY2FsbGJhY2tfcmVkdWNlX2xyX29uX3BsYXRlYXUocGF0aWVuY2UgPSAzKQogICAgKQogICkKYGBgCgpZb3Ugc2hvdWxkIHNlZSBhbiBpbXByb3ZlbWVudCBvdmVyIGFuZCBhYm92ZSB0aGUgYmVuY2htYXJrIG1vZGVsLiAKCmBgYHtyfQpiZXN0X2Vwb2NoIDwtIHdoaWNoKG0xX2hpc3RvcnkkbWV0cmljcyR2YWxfbG9zcyA9PSBtaW4obTFfaGlzdG9yeSRtZXRyaWNzJHZhbF9sb3NzKSkKbG9zcyA8LSBtMV9oaXN0b3J5JG1ldHJpY3MkdmFsX2xvc3NbYmVzdF9lcG9jaF0gJT4lIHJvdW5kKDMpCmFjYyA8LSBtMV9oaXN0b3J5JG1ldHJpY3MkdmFsX2FjY3VyYWN5W2Jlc3RfZXBvY2hdICU+JSByb3VuZCgzKQoKZ2x1ZSgiVGhlIGJlc3QgZXBvY2ggaGFkIGEgbG9zcyBvZiB7bG9zc30gYW5kIG1lYW4gYWJzb2x1dGUgZXJyb3Igb2Yge2FjY30iKQpgYGAKCiMgTW9kZWxpbmcgd2l0aCBzZXF1ZW5jZSBlbWJlZGRpbmdzIHVzaW5nIExTVE1zCgpMZXQncyBtb2RpZnkgb3VyIG1vZGVsIHNvIHRoYXQgaW4gYWRkaXRpb24gdG8gZW1iZWRkaW5nIG91ciBxdWVzdGlvbnMsIHdlIGNhbgp1c2UgTFNUTXMgdG8gZW1iZWQgb3VyIHNlcXVlbmNlcy4gV2UgZG8gdGhpcyBieSBhZGRpbmcgYW4gTFNUTSBsYXllciBhZnRlciB0aGUKZW1iZWRkaW5nIGxheWVyIGFzIHJlcHJlc2VudGVkIGhlcmU6CgpgYGB7ciwgZWNobz1GQUxTRX0Ka25pdHI6OmluY2x1ZGVfZ3JhcGhpY3MoInAyLW1vZGVsMi1zdHJ1Y3R1cmUucG5nIikKYGBgCgpUaGUgaWRlYSBiZWhpbmQgdGhpcyBpcyB0aGF0IHRoZSBMU1RNIHNlcXVlbmNlIGVtYmVkZGluZ3MgbWF5IGFsbG93IHVzIHRvIGltcHJvdmUKdGhlIGFiaWxpdHkgdG8gY2FwdHVyZSBxdWVzdGlvbnMgd2l0aCBzdHJvbmcgc2ltaWxhcml0aWVzIGJ1dCB3b3JkZWQgaW4KY29udHJhc3Rpbmcgb3JkZXIgYW5kL29yIGxlbmd0aC4gRm9yIGV4YW1wbGU6Cgo+ICJfSSBhbSBhIDEzLXllYXItb2xkIGJveSB0aGF0IHdhbnRzIHRvIGxlYXJuIGhvdyB0byBwcm9ncmFtIHZpZGVvIGdhbWVzLiBXaGF0CnByb2dyYW1taW5nIGxhbmd1YWdlcyBzaG91bGQgSSBsZWFybj8gSG93IGRvIEkgZ2V0IHN0YXJ0ZWQ/XyIKCj4gIl9JIGFtIGVudGVyaW5nIHRoZSB3b3JsZCBvZiB2aWRlbyBnYW1lIHByb2dyYW1taW5nIGFuZCB3YW50IHRvIGtub3cgd2hhdApsYW5ndWFnZSBJIHNob3VsZCBsZWFybj8gQmVjYXVzZSB0aGVyZSBhcmUgc28gbWFueSBsYW5ndWFnZXMgSSBkbyBub3Qga25vdyB3aGljaApvbmUgdG8gc3RhcnQgd2l0aC4gQ2FuIHlvdSByZWNvbW1lbmQgYSBsYW5ndWFnZSB0aGF0J3MgZWFzeSB0byBsZWFybiBhbmQgY2FuIGJlCnVzZWQgd2l0aCBtYW55IHBsYXRmb3Jtcz9fIgoKVG8gY3JlYXRlIHRoaXMgbW9kZWwgc3RydWN0dXJlLCB3ZSBjYW4gdXNlIHRoZSBzYW1lIGlucHV0IChgaW5wdXRfcTFgICYgYGlucHV0X3EyYCkKYW5kIHdvcmQgZW1iZWRkaW5nIChgcTFfZW1iZWRkaW5nc2AgJiBgcTJfZW1iZWRkaW5nc2ApIG9iamVjdHMgY3JlYXRlZCBlYXJsaWVyLgpIb3dldmVyLCBzaW5jZSBvdXIgd29yZCBlbWJlZGRpbmdzIHNob3dlZCBzaWducyBvZiBvdmVyZml0dGluZywgd2UgY2FuIGNyZWF0ZQpuZXcgd29yZCBlbWJlZGRpbmcgb2JqZWN0cyB0aGF0IGluY2x1ZGUgcmVndWxhcml6YXRpb24gKGFub3RoZXIgaHlwZXJwYXJhbWV0ZXIKdGhhdCBjb3VsZCBiZSBhZGp1c3RlZCkuCgpOb3csIGluc3RlYWQgb2YgZmxhdHRlbmluZyB0aGUgd29yZCBlbWJlZGRpbmdzLCB3ZSB3aWxsIHNpbXBseSBmZWVkIHRoZW0gaW50bwpMU1RNIGxheWVycywgd2hpY2ggeW91IGNhbiBhbHNvIGFkZCByZWd1bGFyaXphdGlvbiBpZiBkZXNpcmVkLiBTZXQgdGhlIHNpemUgb2YKdGhlIExTVE0gbGF5ZXJzIHRvIHRoYXQgc3BlY2lmaWVkIHdpdGggYGxzdG1fc2l6ZWAgKGxpbmUgNTEyKS4gVGhlIG91dHB1dCBvZiBhbgpMU1RNIGxheWVyIGlzIGEgdmVjdG9yICgxRCB0ZW5zb3IpIHNvIHdlIGRvIG5vdCBuZWVkIHRvIGZsYXR0ZW4gaXQgcHJpb3IgdG8KZmVlZGluZyBpdCBpbnRvIHRoZSBkb3QgcHJvZHVjdCwgc28gdGhlIHJlc3Qgb2YgdGhlIGNvZGUgaXMgYXMgaXQgd2FzIGVhcmxpZXIuCgpgYGB7cn0KIyB3b3JkIGVtYmVkZGluZ3MKcTFfZW1iZWRkaW5ncyA8LSBpbnB1dF9xMSAlPiUgCiAgbGF5ZXJfZW1iZWRkaW5nKAogICAgaW5wdXRfZGltID0gdm9jYWJfc2l6ZSArIDIsCiAgICBvdXRwdXRfZGltID0gZW1iZWRkaW5nX3NpemUsCiAgICBpbnB1dF9sZW5ndGggPSBtYXhfbGVuLAogICAgZW1iZWRkaW5nc19yZWd1bGFyaXplciA9IHJlZ3VsYXJpemVyX2wyKDAuMDAwMSksCiAgICBuYW1lID0gIlExX2VtYmVkZGluZ3MiCiAgKQoKcTJfZW1iZWRkaW5ncyA8LSBpbnB1dF9xMiAlPiUgCiAgbGF5ZXJfZW1iZWRkaW5nKAogICAgaW5wdXRfZGltID0gdm9jYWJfc2l6ZSArIDIsCiAgICBvdXRwdXRfZGltID0gZW1iZWRkaW5nX3NpemUsCiAgICBpbnB1dF9sZW5ndGggPSBtYXhfbGVuLAogICAgZW1iZWRkaW5nc19yZWd1bGFyaXplciA9IHJlZ3VsYXJpemVyX2wyKDAuMDAwMSksCiAgICBuYW1lID0gIlEyX2VtYmVkZGluZ3MiCiAgKQoKIyBzZXF1ZW5jZSBlbWJlZGRpbmdzCnExX2xzdG0gPC0gcTFfZW1iZWRkaW5ncyAlPiUKICBsYXllcl9sc3RtKAogICAgdW5pdHMgPSBsc3RtX3NpemUsIAogICAga2VybmVsX3JlZ3VsYXJpemVyID0gcmVndWxhcml6ZXJfbDIoMC4wMDAxKSwgCiAgICBuYW1lID0gIlExX2xzdG0iCiAgICApCgpxMl9sc3RtIDwtIHEyX2VtYmVkZGluZ3MgJT4lCiAgbGF5ZXJfbHN0bSgKICAgIHVuaXRzID0gbHN0bV9zaXplLCAKICAgIGtlcm5lbF9yZWd1bGFyaXplciA9IHJlZ3VsYXJpemVyX2wyKDAuMDAwMSksICAKICAgIG5hbWUgPSAiUTJfbHN0bSIKICAgICkKCiMgZG90IHByb2R1Y3QKZG90IDwtIGxheWVyX2RvdChsaXN0KHExX2xzdG0sIHEyX2xzdG0pLCBheGVzID0gMSwgbmFtZSA9ICJkb3RfcHJvZHVjdCIpCgojIG91dHB1dC9wcmVkaWN0aW9uCnByZWQgPC0gZG90ICU+JSBsYXllcl9kZW5zZSh1bml0cyA9IDEsIGFjdGl2YXRpb24gPSAic2lnbW9pZCIsIG5hbWUgPSAic2ltaWxhcml0eV9wcmVkaWN0aW9uIikKYGBgCgpOb3cgY3JlYXRlIGFuZCBjb21waWxlIHRoZSBtb2RlbCBhcyBiZWZvcmUuCgpgYGB7cn0KbHN0bV9tb2RlbCA8LSBrZXJhc19tb2RlbChsaXN0KGlucHV0X3ExLCBpbnB1dF9xMiksIHByZWQpCmxzdG1fbW9kZWwgJT4lIGNvbXBpbGUoCiAgb3B0aW1pemVyID0gInJtc3Byb3AiLCAKICBsb3NzID0gImJpbmFyeV9jcm9zc2VudHJvcHkiLCAKICBtZXRyaWNzID0gImFjY3VyYWN5IgopCgpzdW1tYXJ5KGxzdG1fbW9kZWwpCmBgYAoKTm93IHRyYWluIHRoZSBtb2RlbCBhcyBiZWZvcmUuIE5vdGUgaG93IG11Y2ggc2xvd2VyIHRyYWluaW5nIHRpbWUgaXMgd2hlbiB3ZSBhZGQKTFNUTSBsYXllcnMhCgpgYGB7cn0KbTJfaGlzdG9yeSA8LSBsc3RtX21vZGVsICU+JQogIGZpdCgKICAgIGxpc3QodHJhaW5fcXVlc3Rpb24xX3BhZGRlZCwgdHJhaW5fcXVlc3Rpb24yX3BhZGRlZCksCiAgICB0cmFpbl9yZXNwb25zZSwgCiAgICBiYXRjaF9zaXplID0gNjQsIAogICAgZXBvY2hzID0gMjAsIAogICAgdmFsaWRhdGlvbl9kYXRhID0gbGlzdCgKICAgICAgbGlzdCh2YWxfcXVlc3Rpb24xX3BhZGRlZCwgdmFsX3F1ZXN0aW9uMl9wYWRkZWQpLCAKICAgICAgdmFsX3Jlc3BvbnNlCiAgICApLAogICAgY2FsbGJhY2tzID0gbGlzdCgKICAgICAgY2FsbGJhY2tfZWFybHlfc3RvcHBpbmcocGF0aWVuY2UgPSA1LCByZXN0b3JlX2Jlc3Rfd2VpZ2h0cyA9IFRSVUUpLAogICAgICBjYWxsYmFja19yZWR1Y2VfbHJfb25fcGxhdGVhdShwYXRpZW5jZSA9IDMpCiAgICApCiAgKQpgYGAKCllvdSBzaG91bGQgc2VlIGFuIGltcHJvdmVtZW50IG92ZXIgYW5kIGFib3ZlIHRoZSBiZW5jaG1hcmsgbW9kZWwuIAoKYGBge3J9CmJlc3RfZXBvY2ggPC0gd2hpY2gobTJfaGlzdG9yeSRtZXRyaWNzJHZhbF9sb3NzID09IG1pbihtMl9oaXN0b3J5JG1ldHJpY3MkdmFsX2xvc3MpKQpsb3NzIDwtIG0yX2hpc3RvcnkkbWV0cmljcyR2YWxfbG9zc1tiZXN0X2Vwb2NoXSAlPiUgcm91bmQoMykKYWNjIDwtIG0yX2hpc3RvcnkkbWV0cmljcyR2YWxfYWNjdXJhY3lbYmVzdF9lcG9jaF0gJT4lIHJvdW5kKDMpCgpnbHVlKCJUaGUgYmVzdCBlcG9jaCBoYWQgYSBsb3NzIG9mIHtsb3NzfSBhbmQgbWVhbiBhYnNvbHV0ZSBlcnJvciBvZiB7YWNjfSIpCmBgYAoKIyBOZXh0IHN0ZXBzCgpPbmNlIHlvdSBnZXQgdGhlIGFib3ZlIG1vZGVscyB3b3JraW5nIG9uIGEgc21hbGxlciBmcmFjdGlvbiBvZiB0aGUgZGF0YSwgZ28KYWhlYWQgYW5kIHRyYWluIHRoZW0gb24gdGhlIGVudGlyZSBkYXRhc2V0ICh5b3UnbGwgbmVlZCB0byByZS1ydW4gdGhlCnByZXByb2Nlc3Npbmcgc3RlcHMpLiBGZWVsIGZyZWUgdG8gdHVuZSB0aGUgaHlwZXJwYXJhbWV0ZXJzIHRvIGZpbmQgYSBuZWFyCm9wdGltYWwgbW9kZWwgKGFsdGhvdWdoIHRpbWUgd2lsbCBiZSBhIGNvbnN0cmFpbnQpLiBJZiB5b3UgZmluZCB0aGUgcmlnaHQgbW9kZWwKeW91IHdpbGwgYmUgYWJsZSB0byBnZXQgbmVhciA4NSUgYWNjdXJhY3kuCgpBbHRob3VnaCB0cmFpbmluZyB0aW1lIGZvciB0aGVzZSBtb2RlbHMgY2FuIGJlIHF1aXRlIHNsb3csIGFwcGx5aW5nIHRoZSBtb2RlbHMKdG8gbmV3IGlucHV0IChha2EgInNjb3JpbmciKSBpcyBxdWl0ZSBmYXN0LiBGb3IgZXhhbXBsZSwgdGhlIGZvbGxvd2luZyBmdW5jdGlvbgpjYW4gYmUgdXNlZCB0byBtYWtlIHByZWRpY3Rpb25zLiBJbiB0aGlzIGZ1bmN0aW9uIHdlIHByZXByb2Nlc3MgdGhlIGlucHV0IGRhdGEKaW4gdGhlIHNhbWUgd2F5IHdlIHByZXByb2Nlc3NlZCB0aGUgdHJhaW5pbmcgZGF0YSAoYHRva2VuaXplcmApLgoKYGBge3J9CnByZWRpY3RfcXVlc3Rpb25fcGFpcnMgPC0gZnVuY3Rpb24obW9kZWwsIHRva2VuaXplciwgcTEsIHEyKSB7CiAgcTEgPC0gdGV4dHNfdG9fc2VxdWVuY2VzKHRva2VuaXplciwgbGlzdChxMSkpCiAgcTIgPC0gdGV4dHNfdG9fc2VxdWVuY2VzKHRva2VuaXplciwgbGlzdChxMikpCiAgCiAgcTEgPC0gcGFkX3NlcXVlbmNlcyhxMSwgMjApCiAgcTIgPC0gcGFkX3NlcXVlbmNlcyhxMiwgMjApCiAgCiAgYXMubnVtZXJpYyhwcmVkaWN0KG1vZGVsLCBsaXN0KHExLCBxMikpKQp9CmBgYAoKV2UgY2FuIG5vdyBjYWxsIGl0IHdpdGggbmV3IHBhaXJzIG9mIHF1ZXN0aW9ucywgZm9yIGV4YW1wbGU6CgpgYGB7cn0KUTEgPC0gIldoYXQncyBSIHByb2dyYW1taW5nPyIKUTIgPC0gIldoYXQgaXMgUiBwcm9ncmFtbWluZz8iCgpwcmVkIDwtIHByZWRpY3RfcXVlc3Rpb25fcGFpcnMoZW1iZWRkaW5nX21vZGVsLCB0b2tlbml6ZXIsIFExLCBRMikKCmdsdWUoIk91ciBtb2RlbCBzdWdnZXN0cyB0aGF0IHRoZSBzdXBwbGllZCBxdWVzdGlvbnMgaGF2ZSBhIHtyb3VuZChwcmVkLCAyKX0iLAogICAgICJwcm9iYWJpbGl0eSBvZiBiZWluZyBkdXBsaWNhdGVzLiIsIC5zZXAgPSAiICIpCmBgYAoKIyBEZXBsb3kgdGhlIG1vZGVsCgpUbyBkZW1vbnN0cmF0ZSBkZXBsb3ltZW50IG9mIHRoZSB0cmFpbmVkIG1vZGVsLCBJIGNyZWF0ZWQgYSBzaW1wbGUgU2hpbnkKYXBwbGljYXRpb24sIHdoZXJlIHlvdSBjYW4gcGFzdGUgMiBxdWVzdGlvbnMgZnJvbSBRdW9yYSBhbmQgZmluZCB0aGUgcHJvYmFiaWxpdHkKb2YgdGhlbSBiZWluZyBkdXBsaWNhdGVzLiBGb2xsb3cgdGhlc2Ugc3RlcHMgdG8gcnVuIHRoZSBTaGlueSBhcHA6CgoxLiBTYXZlIHlvdXIgZmluYWwgcHJlcHJvY2Vzc2luZyB0b2tlbml6ZXIgdG8gdGhlIGBkbC1rZXJhcy10Zi9tYXRlcmlhbHMvMDktcHJvamVjdGAKZGlyZWN0b3J5LgpgYGB7cn0Kc2F2ZV90ZXh0X3Rva2VuaXplcih0b2tlbml6ZXIsICJ0b2tlbml6ZXItcXVlc3Rpb24tcGFpcnMiKQpgYGAKCjIuIFNhdmUgeW91ciBmaW5hbCBtb2RlbCB0byB0aGUgYGRsLWtlcmFzLXRmL21hdGVyaWFscy8wOS1wcm9qZWN0YCBkaXJlY3RvcnkuCmBgYHtyfQpzYXZlX21vZGVsX2hkZjUobW9kZWwsICJtb2RlbC1xdWVzdGlvbi1wYWlycy5oZGY1IikKYGBgCgozLiBOb3cgbGF1bmNoIHRoZSBTaGlueSBhcHAgYnkgcnVubmluZyB0aGUgYGFwcC1xdWVzdGlvbi1wYWlycy5SbWRgIGZpbGUgaW4gdGhlCmBkbC1rZXJhcy10Zi9tYXRlcmlhbHMvMDktcHJvamVjdGAgZGlyZWN0b3J5LgoKLS0tCgpTcGVha2luZyBvZiBkdXBsaWNhdGlvbl5bVGhpcyBwcm9qZWN0IGlzIGJhc2VkIG9uIERhbmllbCBGYWxiZWwncyBibG9nIHBvc3QKIkNsYXNzaWZ5aW5nIER1cGxpY2F0ZSBRdWVzdGlvbnMgZnJvbSBRdW9yYSB3aXRoIEtlcmFzIiBvbiB0aGUgW1RlbnNvckZsb3cgZm9yIFIKYmxvZ10oaHR0cHM6Ly9ibG9ncy5yc3R1ZGlvLmNvbS90ZW5zb3JmbG93LyksIGEgZ3JlYXQgcmVzb3VyY2UgdG8gbGVhcm4gZnJvbSFd