Introduction

This notebook is motivated by a question on OR Stack Exchange, asking about finding a maximum cut in an undirected graph subject to a “budget”. Specifically, given a graph \(G=(V,E)\) with vertex set \(V=\lbrace 1,\dots,N\rbrace\) and a “budget” \(b\in \lbrace 1,\dots,N-1\rbrace\), find the subset \(A\subset V\) which maximizes the size of the cut defined by \(A\) and \(B=V\backslash A\) (i.e., the number of edges \((i,j)\in E\) such that \(i\in A\) and \(j\in B\)) is maximal, subject to the requirement that \(|A|=b\). The question asks about heuristic methods to find a cut.

One possible heuristic that comes to mind is a genetic algorithm (GA) using a permutation of \(V\) as the chromosome, with the first \(b\) vertices in the permutation forming \(A\). To test it, we will generate a random graph, find an optimal cut by solving a mixed-integer linear program (MILP) using CPLEX, and then use a GA to find a “good” cut and see how good it is.

Preparation

The first step is to load some libraries.

# Libraries used to set up and solve the MILP model.
library(ROI)               # general solver interface
library(ROI.plugin.cplex)  # connects the ROI package to CPLEX
library(ompr)              # MIP modeling package
library(ompr.roi)          # connects OMPR to ROI
# Library used to create and run a genetic algorithm.
library(GA)
# Library used for timing the GA.
library(tictoc)
# Library used to set a time limit.
library(R.utils)

Test graph

We need to set some parameters:

N <- 50
b <- 10
delta <- 0.35
seed <- 465
timeLimit <- 120 # seconds

Next, we randomly create a test graph and a budget. The graph will be stored as a \(|E|\times 2\) matrix where each row denotes an edge and the columns are the lower and higher indexed nodes on the edge.

# Set the random seed.
set.seed(seed)
# Generate a matrix containing all edges.
edges <- combn(1:N, 2) |> t()
colnames(edges) <- c("lo", "hi")
# Sample the matrix randomly to get the edges included in the graph.
keep <- round(delta * nrow(edges))  # number of rows to keep
keep <- sample.int(nrow(edges), size = keep) # indices of rows to keep
edges <- edges[sort(keep), ]  # retained edges
nEdges <- nrow(edges)  # number of edges
rm(keep)

We will need a function that computes the objective value given a choice of \(A\).

# Input: a vector containing the vertices in A.
# Output: the size of the corresponding cut.
cutSize <- function(x) {
  sum(xor(edges[, "lo"] %in% x, edges[, "hi"] %in% x))
}

Genetic algorithm

We need a function to take a chromosome (permutation of the vertex numbers) and compute the size of the resulting cut.

# Input: a chromosome (permutation of 1,..., N)
# Output: the size of the cut obtained by defining A as the first b vertices in the permutation
cutFitness <- function(perm) {
  cutSize(perm[1:b])
}

We can now define an “island” GA with a permutation chromosome. We will use defaults for most of the parameters, but stopping after 200 generations with no improvement (with overall generation limit 10,000), using a mutation rate of 0.2 (twice the default), and with parallel computation of fitness values set to true. We also specify a random seed for reproducibility.

gaSeed <- seed
# Start timing.
tic("GA run time")
alg <- gaisl(type = "permutation", fitness = cutFitness, lower = 1, upper = N, maxiter = 10000, run = 200, parallel = TRUE, pmutation = 0.2, seed = gaSeed)
Islands GA | epoch = 1 
Mean = 139.32 | Best = 162.00 
Mean = 139.32 | Best = 162.00 
Mean = 139.32 | Best = 162.00 
Mean = 139.32 | Best = 162.00 
Islands GA | epoch = 2 
Mean = 142.52 | Best = 162.00 
Mean = 140.72 | Best = 162.00 
Mean = 141.72 | Best = 164.00 
Mean = 143.24 | Best = 162.00 
Islands GA | epoch = 3 
Mean = 143.44 | Best = 162.00 
Mean = 143.16 | Best = 162.00 
Mean = 140.60 | Best = 170.00 
Mean = 136.92 | Best = 170.00 
Islands GA | epoch = 4 
Mean = 142.52 | Best = 170.00 
Mean = 141.88 | Best = 162.00 
Mean = 145.84 | Best = 170.00 
Mean = 146.08 | Best = 170.00 
Islands GA | epoch = 5 
Mean = 141.92 | Best = 170.00 
Mean = 142.24 | Best = 170.00 
Mean = 141.44 | Best = 170.00 
Mean = 143.32 | Best = 170.00 
Islands GA | epoch = 6 
Mean = 143.20 | Best = 170.00 
Mean = 141.48 | Best = 170.00 
Mean = 141.56 | Best = 170.00 
Mean = 144.48 | Best = 170.00 
Islands GA | epoch = 7 
Mean = 144.20 | Best = 170.00 
Mean = 144.52 | Best = 170.00 
Mean = 141.76 | Best = 170.00 
Mean = 146.40 | Best = 170.00 
Islands GA | epoch = 8 
Mean = 141.72 | Best = 170.00 
Mean = 139.64 | Best = 170.00 
Mean = 141.40 | Best = 170.00 
Mean = 141.16 | Best = 170.00 
Islands GA | epoch = 9 
Mean = 142.88 | Best = 170.00 
Mean = 143.00 | Best = 171.00 
Mean = 142.56 | Best = 170.00 
Mean = 146.12 | Best = 170.00 
Islands GA | epoch = 10 
Mean = 141.92 | Best = 170.00 
Mean = 136.12 | Best = 171.00 
Mean = 137.84 | Best = 171.00 
Mean = 140.92 | Best = 170.00 
Islands GA | epoch = 11 
Mean = 138.76 | Best = 170.00 
Mean = 144.44 | Best = 171.00 
Mean = 137.92 | Best = 171.00 
Mean = 140.32 | Best = 171.00 
Islands GA | epoch = 12 
Mean = 142.40 | Best = 171.00 
Mean = 142.88 | Best = 171.00 
Mean = 145.48 | Best = 171.00 
Mean = 144.32 | Best = 171.00 
Islands GA | epoch = 13 
Mean = 140.64 | Best = 171.00 
Mean = 142.44 | Best = 171.00 
Mean = 142.36 | Best = 171.00 
Mean = 145.80 | Best = 171.00 
Islands GA | epoch = 14 
Mean = 145.36 | Best = 171.00 
Mean = 141.32 | Best = 171.00 
Mean = 143.92 | Best = 171.00 
Mean = 145.56 | Best = 171.00 
Islands GA | epoch = 15 
Mean = 140.72 | Best = 171.00 
Mean = 142.80 | Best = 171.00 
Mean = 144.88 | Best = 171.00 
Mean = 140.92 | Best = 172.00 
Islands GA | epoch = 16 
Mean = 141.08 | Best = 172.00 
Mean = 142.64 | Best = 171.00 
Mean = 141.04 | Best = 171.00 
Mean = 143.92 | Best = 172.00 
Islands GA | epoch = 17 
Mean = 142.16 | Best = 172.00 
Mean = 143.56 | Best = 172.00 
Mean = 142.28 | Best = 171.00 
Mean = 140.64 | Best = 172.00 
Islands GA | epoch = 18 
Mean = 141.16 | Best = 172.00 
Mean = 142.40 | Best = 172.00 
Mean = 139.96 | Best = 172.00 
Mean = 143.08 | Best = 172.00 
Islands GA | epoch = 19 
Mean = 144.84 | Best = 172.00 
Mean = 144.96 | Best = 172.00 
Mean = 143.92 | Best = 172.00 
Mean = 145.72 | Best = 172.00 
Islands GA | epoch = 20 
Mean = 145.28 | Best = 172.00 
Mean = 144.88 | Best = 172.00 
Mean = 147.44 | Best = 172.00 
Mean = 144.56 | Best = 172.00 
Islands GA | epoch = 21 
Mean = 141.40 | Best = 172.00 
Mean = 139.20 | Best = 172.00 
Mean = 140.96 | Best = 172.00 
Mean = 142.60 | Best = 172.00 
Islands GA | epoch = 22 
Mean = 146.04 | Best = 172.00 
Mean = 140.36 | Best = 172.00 
Mean = 145.92 | Best = 172.00 
Mean = 147.44 | Best = 172.00 
Islands GA | epoch = 23 
Mean = 143.08 | Best = 172.00 
Mean = 140.08 | Best = 172.00 
Mean = 143.40 | Best = 172.00 
Mean = 144.20 | Best = 172.00 
Islands GA | epoch = 24 
Mean = 145.32 | Best = 172.00 
Mean = 141.52 | Best = 172.00 
Mean = 143.64 | Best = 175.00 
Mean = 143.76 | Best = 172.00 
Islands GA | epoch = 25 
Mean = 144.00 | Best = 172.00 
Mean = 146.04 | Best = 172.00 
Mean = 142.28 | Best = 175.00 
Mean = 140.64 | Best = 175.00 
Islands GA | epoch = 26 
Mean = 143.76 | Best = 175.00 
Mean = 147.88 | Best = 172.00 
Mean = 140.92 | Best = 175.00 
Mean = 142.44 | Best = 175.00 
Islands GA | epoch = 27 
Mean = 142.16 | Best = 175.00 
Mean = 145.20 | Best = 175.00 
Mean = 143.60 | Best = 175.00 
Mean = 145.40 | Best = 175.00 
Islands GA | epoch = 28 
Mean = 145.92 | Best = 175.00 
Mean = 144.44 | Best = 175.00 
Mean = 144.84 | Best = 175.00 
Mean = 145.48 | Best = 175.00 
Islands GA | epoch = 29 
Mean = 145.68 | Best = 175.00 
Mean = 143.28 | Best = 175.00 
Mean = 141.76 | Best = 175.00 
Mean = 144.88 | Best = 175.00 
Islands GA | epoch = 30 
Mean = 150.84 | Best = 175.00 
Mean = 144.24 | Best = 177.00 
Mean = 142.80 | Best = 175.00 
Mean = 147.96 | Best = 175.00 
Islands GA | epoch = 31 
Mean = 145.44 | Best = 176.00 
Mean = 145.40 | Best = 177.00 
Mean = 143.56 | Best = 177.00 
Mean = 143.92 | Best = 175.00 
Islands GA | epoch = 32 
Mean = 155.00 | Best = 176.00 
Mean = 153.80 | Best = 177.00 
Mean = 150.76 | Best = 177.00 
Mean = 150.72 | Best = 177.00 
Islands GA | epoch = 33 
Mean = 143.92 | Best = 177.00 
Mean = 144.00 | Best = 177.00 
Mean = 146.52 | Best = 177.00 
Mean = 146.68 | Best = 177.00 
Islands GA | epoch = 34 
Mean = 146.48 | Best = 177.00 
Mean = 147.76 | Best = 177.00 
Mean = 143.44 | Best = 177.00 
Mean = 144.56 | Best = 177.00 
Islands GA | epoch = 35 
Mean = 144.28 | Best = 177.00 
Mean = 147.04 | Best = 177.00 
Mean = 148.76 | Best = 177.00 
Mean = 145.08 | Best = 177.00 
Islands GA | epoch = 36 
Mean = 142.44 | Best = 177.00 
Mean = 144.60 | Best = 177.00 
Mean = 144.44 | Best = 177.00 
Mean = 142.96 | Best = 177.00 
Islands GA | epoch = 37 
Mean = 141.24 | Best = 177.00 
Mean = 144.56 | Best = 177.00 
Mean = 145.52 | Best = 177.00 
Mean = 143.40 | Best = 177.00 
Islands GA | epoch = 38 
Mean = 144.96 | Best = 177.00 
Mean = 140.52 | Best = 177.00 
Mean = 144.00 | Best = 177.00 
Mean = 143.12 | Best = 177.00 
Islands GA | epoch = 39 
Mean = 143.36 | Best = 177.00 
Mean = 142.56 | Best = 177.00 
Mean = 144.36 | Best = 177.00 
Mean = 145.60 | Best = 177.00 
Islands GA | epoch = 40 
Mean = 146.28 | Best = 177.00 
Mean = 144.96 | Best = 177.00 
Mean = 142.08 | Best = 177.00 
Mean = 146.72 | Best = 177.00 
Islands GA | epoch = 41 
Mean = 141.12 | Best = 177.00 
Mean = 140.32 | Best = 177.00 
Mean = 143.52 | Best = 177.00 
Mean = 142.68 | Best = 177.00 
Islands GA | epoch = 42 
Mean = 142.92 | Best = 177.00 
Mean = 141.84 | Best = 177.00 
Mean = 140.48 | Best = 177.00 
Mean = 142.28 | Best = 177.00 
Islands GA | epoch = 43 
Mean = 140.72 | Best = 177.00 
Mean = 144.40 | Best = 177.00 
Mean = 144.80 | Best = 177.00 
Mean = 141.72 | Best = 177.00 
Islands GA | epoch = 44 
Mean = 142.64 | Best = 177.00 
Mean = 145.76 | Best = 177.00 
Mean = 143.52 | Best = 177.00 
Mean = 140.00 | Best = 177.00 
Islands GA | epoch = 45 
Mean = 145.52 | Best = 177.00 
Mean = 142.28 | Best = 177.00 
Mean = 144.76 | Best = 177.00 
Mean = 143.68 | Best = 177.00 
Islands GA | epoch = 46 
Mean = 142.92 | Best = 177.00 
Mean = 147.48 | Best = 177.00 
Mean = 146.36 | Best = 177.00 
Mean = 147.72 | Best = 177.00 
Islands GA | epoch = 47 
Mean = 143.40 | Best = 177.00 
Mean = 143.04 | Best = 177.00 
Mean = 146.00 | Best = 177.00 
Mean = 144.76 | Best = 178.00 
Islands GA | epoch = 48 
Mean = 138.56 | Best = 178.00 
Mean = 139.36 | Best = 177.00 
Mean = 142.08 | Best = 177.00 
Mean = 142.92 | Best = 178.00 
Islands GA | epoch = 49 
Mean = 146.16 | Best = 178.00 
Mean = 146.16 | Best = 178.00 
Mean = 146.36 | Best = 177.00 
Mean = 144.48 | Best = 178.00 
Islands GA | epoch = 50 
Mean = 144.68 | Best = 178.00 
Mean = 145.32 | Best = 178.00 
Mean = 144.12 | Best = 178.00 
Mean = 144.48 | Best = 178.00 
Islands GA | epoch = 51 
Mean = 144.88 | Best = 178.00 
Mean = 145.44 | Best = 178.00 
Mean = 146.76 | Best = 178.00 
Mean = 142.96 | Best = 178.00 
Islands GA | epoch = 52 
Mean = 143.84 | Best = 178.00 
Mean = 140.84 | Best = 178.00 
Mean = 144.88 | Best = 178.00 
Mean = 147.12 | Best = 178.00 
Islands GA | epoch = 53 
Mean = 142.92 | Best = 178.00 
Mean = 143.96 | Best = 178.00 
Mean = 146.80 | Best = 178.00 
Mean = 142.84 | Best = 178.00 
Islands GA | epoch = 54 
Mean = 140.32 | Best = 178.00 
Mean = 145.36 | Best = 178.00 
Mean = 145.48 | Best = 178.00 
Mean = 143.20 | Best = 178.00 
Islands GA | epoch = 55 
Mean = 143.08 | Best = 178.00 
Mean = 142.56 | Best = 178.00 
Mean = 146.80 | Best = 178.00 
Mean = 141.40 | Best = 178.00 
Islands GA | epoch = 56 
Mean = 146.36 | Best = 178.00 
Mean = 146.40 | Best = 178.00 
Mean = 149.44 | Best = 178.00 
Mean = 146.80 | Best = 178.00 
Islands GA | epoch = 57 
Mean = 144.56 | Best = 178.00 
Mean = 144.76 | Best = 178.00 
Mean = 147.52 | Best = 178.00 
Mean = 146.24 | Best = 178.00 
Islands GA | epoch = 58 
Mean = 146.92 | Best = 178.00 
Mean = 145.76 | Best = 178.00 
Mean = 144.40 | Best = 178.00 
Mean = 149.16 | Best = 178.00 
Islands GA | epoch = 59 
Mean = 149.52 | Best = 178.00 
Mean = 146.56 | Best = 178.00 
Mean = 148.24 | Best = 178.00 
Mean = 148.72 | Best = 178.00 
Islands GA | epoch = 60 
Mean = 143.16 | Best = 178.00 
Mean = 143.72 | Best = 178.00 
Mean = 143.24 | Best = 178.00 
Mean = 145.84 | Best = 178.00 
Islands GA | epoch = 61 
Mean = 144.56 | Best = 178.00 
Mean = 146.80 | Best = 178.00 
Mean = 145.32 | Best = 178.00 
Mean = 144.84 | Best = 178.00 
Islands GA | epoch = 62 
Mean = 145.60 | Best = 178.00 
Mean = 146.00 | Best = 178.00 
Mean = 145.92 | Best = 178.00 
Mean = 144.88 | Best = 178.00 
Islands GA | epoch = 63 
Mean = 143.92 | Best = 178.00 
Mean = 142.72 | Best = 178.00 
Mean = 143.80 | Best = 178.00 
Mean = 143.84 | Best = 178.00 
Islands GA | epoch = 64 
Mean = 142.56 | Best = 178.00 
Mean = 146.96 | Best = 178.00 
Mean = 142.04 | Best = 178.00 
Mean = 141.32 | Best = 178.00 
Islands GA | epoch = 65 
Mean = 142.40 | Best = 178.00 
Mean = 142.00 | Best = 178.00 
Mean = 145.52 | Best = 178.00 
Mean = 146.12 | Best = 178.00 
Islands GA | epoch = 66 
Mean = 143.80 | Best = 178.00 
Mean = 146.44 | Best = 178.00 
Mean = 146.28 | Best = 178.00 
Mean = 143.60 | Best = 178.00 
Islands GA | epoch = 67 
Mean = 139.88 | Best = 178.00 
Mean = 142.64 | Best = 178.00 
Mean = 143.56 | Best = 178.00 
Mean = 142.32 | Best = 178.00 
Islands GA | epoch = 68 
Mean = 146.52 | Best = 178.00 
Mean = 143.64 | Best = 178.00 
Mean = 144.08 | Best = 178.00 
Mean = 146.00 | Best = 178.00 
Islands GA | epoch = 69 
Mean = 141.04 | Best = 178.00 
Mean = 146.16 | Best = 178.00 
Mean = 144.40 | Best = 178.00 
Mean = 141.32 | Best = 178.00 
Islands GA | epoch = 70 
Mean = 144.44 | Best = 178.00 
Mean = 140.64 | Best = 178.00 
Mean = 140.60 | Best = 178.00 
Mean = 145.36 | Best = 178.00 
toc()
GA run time: 9.344 sec elapsed
# Record and display the results.
gaValue <- alg@fitnessValue
gaIncumbent <- sort(alg@solution[1, 1:b])
cat("The best cut achieved has size = ", gaValue, "\n")
The best cut achieved has size =  178 
cat("A = ", gaIncumbent)
A =  4 9 13 14 17 30 38 46 48 50

For comparison with the swap heuristic, we will try running the GA with random restarts and a fixed time limit.

# We will use a copy of the random seed, modifying it for each successive run.
gaSeed <- seed
tic("Repeated GA")
withTimeout(
  {
    # Loop until time runs out.
    while (TRUE) {
      # Increment the seed.
      gaSeed <- gaSeed + 1
      # Run an island GA.
      alg <- gaisl(type = "permutation", fitness = cutFitness, lower = 1, upper = N, maxiter = 10000, run = 200, parallel = TRUE, pmutation = 0.2, seed = gaSeed, monitor = FALSE)
      # Check for a new incumbent.
      if (alg@fitnessValue > gaValue) {
        gaValue <- alg@fitnessValue
        gaIncumbent <- sort(alg@solution[1, 1:b])
      }
    }
    # In case the last GA run was interrupted, check for a new incumbent.
    if (alg@fitnessValue > gaValue) {
      gaValue <- alg@fitnessValue
      gaIncumbent <- sort(alg@solution[1, 1:b])
    }
  },
  timeout = timeLimit, cpu = Inf, onTimeout = "silent")
NULL
toc()
Repeated GA: 120.186 sec elapsed
cat("GA incumbent value = ", gaValue, "\n")
GA incumbent value =  178 
cat("GA solution for A = ", gaIncumbent, "\n")
GA solution for A =  4 9 13 14 17 30 38 46 48 50 

Simple swap heuristic

Next, we will test a simple heuristic that generates a random choice of \(A\), then considers pairwise swaps of a vertex in \(A\) with a vertex in \(B\), accepting the swaps that improve the objective function. If no swap causes any improvement and a predefined time limit has not been reached, we restart with a new random \(A\).

# We need to track the incumbent objective value and the incumbent choice of $A$.
swapIncumbent <- NA
swapValue <- 0
# We reset the random seed for reproducibility.
set.seed(seed)
# Run the heuristic with the given time limit.
withTimeout(
  {
    # Loop until time runs out.
    while (TRUE) {
      # Generate a new random choice of A.
      A <- sample(1:N, b)
      # Test whether it is a new incumbent.
      aValue <- cutSize(A)
      if (aValue > swapValue) {
        swapValue <- aValue
        swapIncumbent <- A
      }
      # Set a flag to try swapping.
      trySwapping <- TRUE
      # Loop until a full pass occurs with no successful swaps.
      while (trySwapping) {
        trySwapping <- FALSE
        restart <- FALSE
        for (i in A) {
          for (j in setdiff(1:N, A)) {
            # Swap j for i.
            temp <- c(setdiff(A, i), j)
            # Compute the objective value.
            z <- cutSize(temp);
            # If it is an improvement, accept it.
            if (z > aValue) {
              A <- temp
              aValue <- z
              # Check for a new incumbent.
              if (z > swapValue) {
                swapValue <- z
                swapIncumbent <- temp
              }
              # A successful swap means we break out of the for loops and continue looking for swaps.
              trySwapping <- TRUE
              restart <- TRUE
              break # exits the inner for loop
            }
          }
          if (restart) break  # breaks out of the outer for loop
        }
      }
    }
  },
  timeout = timeLimit, cpu = Inf, onTimeout = "silent")
NULL
cat("Heuristic incumbent value = ", swapValue, "\n")
Heuristic incumbent value =  181 
cat("Heuristic solution for A = ", swapIncumbent, "\n")
Heuristic solution for A =  4 50 44 13 14 34 17 30 48 43 

MILP model

The MILP model uses a 0-1 variable (\(x\)) for each node (1 if the node belongs to \(A\), 0 if not) and a continuous variable (\(y\)) with domain \([0,1]\) for each edge (1 if the edge is part of the cut, 0 if not).

# Initialize the model.
mip <- MILPModel() |>
# Add the variables.
         add_variable(x.var[i], i = 1:N, type = "binary") |>
         add_variable(y.var[i], i = 1:nEdges, type = "continuous", lb = 0, ub = 1) |>
# Set the objective function.
         set_objective(sum_expr(y.var[i], i = 1:nEdges), sense = "max") |>
# Add the budget constraint.
         add_constraint(sum_expr(x.var[i], i = 1:N) == b)
# For each edge, add two constraints connecting the y variable for that edge to the x variables for its two endpoints.
for (i in 1:nEdges) {
  lo <- edges[i, "lo"]
  hi <- edges[i, "hi"]
  mip <- mip |>
           add_constraint(y.var[i] - x.var[lo] - x.var[hi] <= 0) |>
           add_constraint(y.var[i] + x.var[lo] + x.var[hi] <= 2)
}
tic("CPLEX run time")
# Solve the model.
solution <- mip |> solve_model(with_ROI(solver = "cplex"))
CPLEX environment opened
Closed CPLEX environment
toc()
CPLEX run time: 3.167 sec elapsed
# Get the objective value (cut size).
cat("The optimal cut size = ", solution$objective_value, "\n")
The optimal cut size =  181 
# Recover the choice of A.
temp <- (solution |> get_solution(x.var[j]))[, "value"]
A <- which(temp > 0.5)
rm(temp, lo, hi)
cat("The optimal choice for A = ", A, "\n")
The optimal choice for A =  4 12 13 14 30 38 43 44 48 50 
LS0tCnRpdGxlOiAiTWF4IEN1dCB3aXRoIEJ1ZGdldCIKb3V0cHV0OiBodG1sX25vdGVib29rCmF1dGhvcjogUGF1bCBBLiBSdWJpbgpkYXRlOiBOb3ZlbWJlciAxMiwgMjAyMQotLS0KIyBJbnRyb2R1Y3Rpb24KClRoaXMgbm90ZWJvb2sgaXMgbW90aXZhdGVkIGJ5IGEgW3F1ZXN0aW9uXShodHRwczovL29yLnN0YWNrZXhjaGFuZ2UuY29tL3F1ZXN0aW9ucy83MjY5L2J1ZGdldGVkLW1heC1jdXQtaGV1cmlzdGljLzcyNzMpIG9uIE9SIFN0YWNrIEV4Y2hhbmdlLCBhc2tpbmcgYWJvdXQgZmluZGluZyBhIG1heGltdW0gY3V0IGluIGFuIHVuZGlyZWN0ZWQgZ3JhcGggc3ViamVjdCB0byBhICJidWRnZXQiLiBTcGVjaWZpY2FsbHksIGdpdmVuIGEgZ3JhcGggJEc9KFYsRSkkIHdpdGggdmVydGV4IHNldCAkVj1cbGJyYWNlIDEsXGRvdHMsTlxyYnJhY2UkIGFuZCBhICJidWRnZXQiICRiXGluIFxsYnJhY2UgMSxcZG90cyxOLTFccmJyYWNlJCwgZmluZCB0aGUgc3Vic2V0ICRBXHN1YnNldCBWJCB3aGljaCBtYXhpbWl6ZXMgdGhlIHNpemUgb2YgdGhlIGN1dCBkZWZpbmVkIGJ5ICRBJCBhbmQgJEI9VlxiYWNrc2xhc2ggQSQgKGkuZS4sIHRoZSBudW1iZXIgb2YgZWRnZXMgJChpLGopXGluIEUkIHN1Y2ggdGhhdCAkaVxpbiBBJCBhbmQgJGpcaW4gQiQpIGlzIG1heGltYWwsIHN1YmplY3QgdG8gdGhlIHJlcXVpcmVtZW50IHRoYXQgJHxBfD1iJC4gVGhlIHF1ZXN0aW9uIGFza3MgYWJvdXQgaGV1cmlzdGljIG1ldGhvZHMgdG8gZmluZCBhIGN1dC4KCk9uZSBwb3NzaWJsZSBoZXVyaXN0aWMgdGhhdCBjb21lcyB0byBtaW5kIGlzIGEgZ2VuZXRpYyBhbGdvcml0aG0gKEdBKSB1c2luZyBhIHBlcm11dGF0aW9uIG9mICRWJCBhcyB0aGUgY2hyb21vc29tZSwgd2l0aCB0aGUgZmlyc3QgJGIkIHZlcnRpY2VzIGluIHRoZSBwZXJtdXRhdGlvbiBmb3JtaW5nICRBJC4gVG8gdGVzdCBpdCwgd2Ugd2lsbCBnZW5lcmF0ZSBhIHJhbmRvbSBncmFwaCwgZmluZCBhbiBvcHRpbWFsIGN1dCBieSBzb2x2aW5nIGEgbWl4ZWQtaW50ZWdlciBsaW5lYXIgcHJvZ3JhbSAoTUlMUCkgdXNpbmcgQ1BMRVgsIGFuZCB0aGVuIHVzZSBhIEdBIHRvIGZpbmQgYSAiZ29vZCIgY3V0IGFuZCBzZWUgaG93IGdvb2QgaXQgaXMuCgojIFByZXBhcmF0aW9uCgpUaGUgZmlyc3Qgc3RlcCBpcyB0byBsb2FkIHNvbWUgbGlicmFyaWVzLgoKYGBge3J9CiMgTGlicmFyaWVzIHVzZWQgdG8gc2V0IHVwIGFuZCBzb2x2ZSB0aGUgTUlMUCBtb2RlbC4KbGlicmFyeShST0kpICAgICAgICAgICAgICAgIyBnZW5lcmFsIHNvbHZlciBpbnRlcmZhY2UKbGlicmFyeShST0kucGx1Z2luLmNwbGV4KSAgIyBjb25uZWN0cyB0aGUgUk9JIHBhY2thZ2UgdG8gQ1BMRVgKbGlicmFyeShvbXByKSAgICAgICAgICAgICAgIyBNSVAgbW9kZWxpbmcgcGFja2FnZQpsaWJyYXJ5KG9tcHIucm9pKSAgICAgICAgICAjIGNvbm5lY3RzIE9NUFIgdG8gUk9JCiMgTGlicmFyeSB1c2VkIHRvIGNyZWF0ZSBhbmQgcnVuIGEgZ2VuZXRpYyBhbGdvcml0aG0uCmxpYnJhcnkoR0EpCiMgTGlicmFyeSB1c2VkIGZvciB0aW1pbmcgdGhlIEdBLgpsaWJyYXJ5KHRpY3RvYykKIyBMaWJyYXJ5IHVzZWQgdG8gc2V0IGEgdGltZSBsaW1pdC4KbGlicmFyeShSLnV0aWxzKQpgYGAKCiMgVGVzdCBncmFwaAoKV2UgbmVlZCB0byBzZXQgc29tZSBwYXJhbWV0ZXJzOgoKKiB0aGUgc2l6ZSBvZiB0aGUgZ3JhcGggKCROJCk7CiogdGhlICJidWRnZXQiICgkYiQpOwoqIHRoZSBkZW5zaXR5IG9mIHRoZSBncmFwaCAoJFxkZWx0YSQsIHNvIHRoYXQgJHxFfFxhcHByb3ggMC41KlxkZWx0YSAqIE4gKiAoTi0xKSQpOwoqIGEgc2VlZCBmb3IgdGhlIHJhbmRvbSBudW1iZXIgZ2VuZXJhdG9yOyBhbmQKKiBhIHRpbWUgbGltaXQgZm9yIGhldXJpc3RpY3MgdGhhdCBpbmNsdWRlIHJhbmRvbSByZXN0YXJ0cy4KCmBgYHtyfQpOIDwtIDUwCmIgPC0gMTAKZGVsdGEgPC0gMC4zNQpzZWVkIDwtIDQ2NQp0aW1lTGltaXQgPC0gMTIwICMgc2Vjb25kcwpgYGAKCk5leHQsIHdlIHJhbmRvbWx5IGNyZWF0ZSBhIHRlc3QgZ3JhcGggYW5kIGEgYnVkZ2V0LiBUaGUgZ3JhcGggd2lsbCBiZSBzdG9yZWQgYXMgYSAkfEV8XHRpbWVzIDIkIG1hdHJpeCB3aGVyZSBlYWNoIHJvdyBkZW5vdGVzIGFuIGVkZ2UgYW5kIHRoZSBjb2x1bW5zIGFyZSB0aGUgbG93ZXIgYW5kIGhpZ2hlciBpbmRleGVkIG5vZGVzIG9uIHRoZSBlZGdlLgoKYGBge3J9CiMgU2V0IHRoZSByYW5kb20gc2VlZC4Kc2V0LnNlZWQoc2VlZCkKIyBHZW5lcmF0ZSBhIG1hdHJpeCBjb250YWluaW5nIGFsbCBlZGdlcy4KZWRnZXMgPC0gY29tYm4oMTpOLCAyKSB8PiB0KCkKY29sbmFtZXMoZWRnZXMpIDwtIGMoImxvIiwgImhpIikKIyBTYW1wbGUgdGhlIG1hdHJpeCByYW5kb21seSB0byBnZXQgdGhlIGVkZ2VzIGluY2x1ZGVkIGluIHRoZSBncmFwaC4Ka2VlcCA8LSByb3VuZChkZWx0YSAqIG5yb3coZWRnZXMpKSAgIyBudW1iZXIgb2Ygcm93cyB0byBrZWVwCmtlZXAgPC0gc2FtcGxlLmludChucm93KGVkZ2VzKSwgc2l6ZSA9IGtlZXApICMgaW5kaWNlcyBvZiByb3dzIHRvIGtlZXAKZWRnZXMgPC0gZWRnZXNbc29ydChrZWVwKSwgXSAgIyByZXRhaW5lZCBlZGdlcwpuRWRnZXMgPC0gbnJvdyhlZGdlcykgICMgbnVtYmVyIG9mIGVkZ2VzCnJtKGtlZXApCmBgYAoKV2Ugd2lsbCBuZWVkIGEgZnVuY3Rpb24gdGhhdCBjb21wdXRlcyB0aGUgb2JqZWN0aXZlIHZhbHVlIGdpdmVuIGEgY2hvaWNlIG9mICRBJC4KCmBgYHtyfQojIElucHV0OiBhIHZlY3RvciBjb250YWluaW5nIHRoZSB2ZXJ0aWNlcyBpbiBBLgojIE91dHB1dDogdGhlIHNpemUgb2YgdGhlIGNvcnJlc3BvbmRpbmcgY3V0LgpjdXRTaXplIDwtIGZ1bmN0aW9uKHgpIHsKICBzdW0oeG9yKGVkZ2VzWywgImxvIl0gJWluJSB4LCBlZGdlc1ssICJoaSJdICVpbiUgeCkpCn0KYGBgCgojIEdlbmV0aWMgYWxnb3JpdGhtCgpXZSBuZWVkIGEgZnVuY3Rpb24gdG8gdGFrZSBhIGNocm9tb3NvbWUgKHBlcm11dGF0aW9uIG9mIHRoZSB2ZXJ0ZXggbnVtYmVycykgYW5kIGNvbXB1dGUgdGhlIHNpemUgb2YgdGhlIHJlc3VsdGluZyBjdXQuCgpgYGB7cn0KIyBJbnB1dDogYSBjaHJvbW9zb21lIChwZXJtdXRhdGlvbiBvZiAxLC4uLiwgTikKIyBPdXRwdXQ6IHRoZSBzaXplIG9mIHRoZSBjdXQgb2J0YWluZWQgYnkgZGVmaW5pbmcgQSBhcyB0aGUgZmlyc3QgYiB2ZXJ0aWNlcyBpbiB0aGUgcGVybXV0YXRpb24KY3V0Rml0bmVzcyA8LSBmdW5jdGlvbihwZXJtKSB7CiAgY3V0U2l6ZShwZXJtWzE6Yl0pCn0KYGBgCgpXZSBjYW4gbm93IGRlZmluZSBhbiAiaXNsYW5kIiBHQSB3aXRoIGEgcGVybXV0YXRpb24gY2hyb21vc29tZS4gV2Ugd2lsbCB1c2UgZGVmYXVsdHMgZm9yIG1vc3Qgb2YgdGhlIHBhcmFtZXRlcnMsIGJ1dCBzdG9wcGluZyBhZnRlciAyMDAgZ2VuZXJhdGlvbnMgd2l0aCBubyBpbXByb3ZlbWVudCAod2l0aCBvdmVyYWxsIGdlbmVyYXRpb24gbGltaXQgMTAsMDAwKSwgdXNpbmcgYSBtdXRhdGlvbiByYXRlIG9mIDAuMiAodHdpY2UgdGhlIGRlZmF1bHQpLCBhbmQgd2l0aCBwYXJhbGxlbCBjb21wdXRhdGlvbiBvZiBmaXRuZXNzIHZhbHVlcyBzZXQgdG8gdHJ1ZS4gV2UgYWxzbyBzcGVjaWZ5IGEgcmFuZG9tIHNlZWQgZm9yIHJlcHJvZHVjaWJpbGl0eS4KCmBgYHtyfQpnYVNlZWQgPC0gc2VlZAojIFN0YXJ0IHRpbWluZy4KdGljKCJHQSBydW4gdGltZSIpCmFsZyA8LSBnYWlzbCh0eXBlID0gInBlcm11dGF0aW9uIiwgZml0bmVzcyA9IGN1dEZpdG5lc3MsIGxvd2VyID0gMSwgdXBwZXIgPSBOLCBtYXhpdGVyID0gMTAwMDAsIHJ1biA9IDIwMCwgcGFyYWxsZWwgPSBUUlVFLCBwbXV0YXRpb24gPSAwLjIsIHNlZWQgPSBnYVNlZWQpCnRvYygpCiMgUmVjb3JkIGFuZCBkaXNwbGF5IHRoZSByZXN1bHRzLgpnYVZhbHVlIDwtIGFsZ0BmaXRuZXNzVmFsdWUKZ2FJbmN1bWJlbnQgPC0gc29ydChhbGdAc29sdXRpb25bMSwgMTpiXSkKY2F0KCJUaGUgYmVzdCBjdXQgYWNoaWV2ZWQgaGFzIHNpemUgPSAiLCBnYVZhbHVlLCAiXG4iKQpjYXQoIkEgPSAiLCBnYUluY3VtYmVudCkKYGBgCkZvciBjb21wYXJpc29uIHdpdGggdGhlIHN3YXAgaGV1cmlzdGljLCB3ZSB3aWxsIHRyeSBydW5uaW5nIHRoZSBHQSB3aXRoIHJhbmRvbSByZXN0YXJ0cyBhbmQgYSBmaXhlZCB0aW1lIGxpbWl0LgoKYGBge3J9CiMgV2Ugd2lsbCB1c2UgYSBjb3B5IG9mIHRoZSByYW5kb20gc2VlZCwgbW9kaWZ5aW5nIGl0IGZvciBlYWNoIHN1Y2Nlc3NpdmUgcnVuLgpnYVNlZWQgPC0gc2VlZAp0aWMoIlJlcGVhdGVkIEdBIikKd2l0aFRpbWVvdXQoCiAgewogICAgIyBMb29wIHVudGlsIHRpbWUgcnVucyBvdXQuCiAgICB3aGlsZSAoVFJVRSkgewogICAgICAjIEluY3JlbWVudCB0aGUgc2VlZC4KICAgICAgZ2FTZWVkIDwtIGdhU2VlZCArIDEKICAgICAgIyBSdW4gYW4gaXNsYW5kIEdBLgogICAgICBhbGcgPC0gZ2Fpc2wodHlwZSA9ICJwZXJtdXRhdGlvbiIsIGZpdG5lc3MgPSBjdXRGaXRuZXNzLCBsb3dlciA9IDEsIHVwcGVyID0gTiwgbWF4aXRlciA9IDEwMDAwLCBydW4gPSAyMDAsIHBhcmFsbGVsID0gVFJVRSwgcG11dGF0aW9uID0gMC4yLCBzZWVkID0gZ2FTZWVkLCBtb25pdG9yID0gRkFMU0UpCiAgICAgICMgQ2hlY2sgZm9yIGEgbmV3IGluY3VtYmVudC4KICAgICAgaWYgKGFsZ0BmaXRuZXNzVmFsdWUgPiBnYVZhbHVlKSB7CiAgICAgICAgZ2FWYWx1ZSA8LSBhbGdAZml0bmVzc1ZhbHVlCiAgICAgICAgZ2FJbmN1bWJlbnQgPC0gc29ydChhbGdAc29sdXRpb25bMSwgMTpiXSkKICAgICAgfQogICAgfQogICAgIyBJbiBjYXNlIHRoZSBsYXN0IEdBIHJ1biB3YXMgaW50ZXJydXB0ZWQsIGNoZWNrIGZvciBhIG5ldyBpbmN1bWJlbnQuCiAgICBpZiAoYWxnQGZpdG5lc3NWYWx1ZSA+IGdhVmFsdWUpIHsKICAgICAgZ2FWYWx1ZSA8LSBhbGdAZml0bmVzc1ZhbHVlCiAgICAgIGdhSW5jdW1iZW50IDwtIHNvcnQoYWxnQHNvbHV0aW9uWzEsIDE6Yl0pCiAgICB9CiAgfSwKICB0aW1lb3V0ID0gdGltZUxpbWl0LCBjcHUgPSBJbmYsIG9uVGltZW91dCA9ICJzaWxlbnQiKQp0b2MoKQpjYXQoIkdBIGluY3VtYmVudCB2YWx1ZSA9ICIsIGdhVmFsdWUsICJcbiIpCmNhdCgiR0Egc29sdXRpb24gZm9yIEEgPSAiLCBnYUluY3VtYmVudCwgIlxuIikKYGBgCgojIFNpbXBsZSBzd2FwIGhldXJpc3RpYwoKTmV4dCwgd2Ugd2lsbCB0ZXN0IGEgc2ltcGxlIGhldXJpc3RpYyB0aGF0IGdlbmVyYXRlcyBhIHJhbmRvbSBjaG9pY2Ugb2YgJEEkLCB0aGVuIGNvbnNpZGVycyBwYWlyd2lzZSBzd2FwcyBvZiBhIHZlcnRleCBpbiAkQSQgd2l0aCBhIHZlcnRleCBpbiAkQiQsIGFjY2VwdGluZyB0aGUgc3dhcHMgdGhhdCBpbXByb3ZlIHRoZSBvYmplY3RpdmUgZnVuY3Rpb24uIElmIG5vIHN3YXAgY2F1c2VzIGFueSBpbXByb3ZlbWVudCBhbmQgYSBwcmVkZWZpbmVkIHRpbWUgbGltaXQgaGFzIG5vdCBiZWVuIHJlYWNoZWQsIHdlIHJlc3RhcnQgd2l0aCBhIG5ldyByYW5kb20gJEEkLgoKYGBge3J9CiMgV2UgbmVlZCB0byB0cmFjayB0aGUgaW5jdW1iZW50IG9iamVjdGl2ZSB2YWx1ZSBhbmQgdGhlIGluY3VtYmVudCBjaG9pY2Ugb2YgJEEkLgpzd2FwSW5jdW1iZW50IDwtIE5BCnN3YXBWYWx1ZSA8LSAwCiMgV2UgcmVzZXQgdGhlIHJhbmRvbSBzZWVkIGZvciByZXByb2R1Y2liaWxpdHkuCnNldC5zZWVkKHNlZWQpCiMgUnVuIHRoZSBoZXVyaXN0aWMgd2l0aCB0aGUgZ2l2ZW4gdGltZSBsaW1pdC4Kd2l0aFRpbWVvdXQoCiAgewogICAgIyBMb29wIHVudGlsIHRpbWUgcnVucyBvdXQuCiAgICB3aGlsZSAoVFJVRSkgewogICAgICAjIEdlbmVyYXRlIGEgbmV3IHJhbmRvbSBjaG9pY2Ugb2YgQS4KICAgICAgQSA8LSBzYW1wbGUoMTpOLCBiKQogICAgICAjIFRlc3Qgd2hldGhlciBpdCBpcyBhIG5ldyBpbmN1bWJlbnQuCiAgICAgIGFWYWx1ZSA8LSBjdXRTaXplKEEpCiAgICAgIGlmIChhVmFsdWUgPiBzd2FwVmFsdWUpIHsKICAgICAgICBzd2FwVmFsdWUgPC0gYVZhbHVlCiAgICAgICAgc3dhcEluY3VtYmVudCA8LSBBCiAgICAgIH0KICAgICAgIyBTZXQgYSBmbGFnIHRvIHRyeSBzd2FwcGluZy4KICAgICAgdHJ5U3dhcHBpbmcgPC0gVFJVRQogICAgICAjIExvb3AgdW50aWwgYSBmdWxsIHBhc3Mgb2NjdXJzIHdpdGggbm8gc3VjY2Vzc2Z1bCBzd2Fwcy4KICAgICAgd2hpbGUgKHRyeVN3YXBwaW5nKSB7CiAgICAgICAgdHJ5U3dhcHBpbmcgPC0gRkFMU0UKICAgICAgICByZXN0YXJ0IDwtIEZBTFNFCiAgICAgICAgZm9yIChpIGluIEEpIHsKICAgICAgICAgIGZvciAoaiBpbiBzZXRkaWZmKDE6TiwgQSkpIHsKICAgICAgICAgICAgIyBTd2FwIGogZm9yIGkuCiAgICAgICAgICAgIHRlbXAgPC0gYyhzZXRkaWZmKEEsIGkpLCBqKQogICAgICAgICAgICAjIENvbXB1dGUgdGhlIG9iamVjdGl2ZSB2YWx1ZS4KICAgICAgICAgICAgeiA8LSBjdXRTaXplKHRlbXApOwogICAgICAgICAgICAjIElmIGl0IGlzIGFuIGltcHJvdmVtZW50LCBhY2NlcHQgaXQuCiAgICAgICAgICAgIGlmICh6ID4gYVZhbHVlKSB7CiAgICAgICAgICAgICAgQSA8LSB0ZW1wCiAgICAgICAgICAgICAgYVZhbHVlIDwtIHoKICAgICAgICAgICAgICAjIENoZWNrIGZvciBhIG5ldyBpbmN1bWJlbnQuCiAgICAgICAgICAgICAgaWYgKHogPiBzd2FwVmFsdWUpIHsKICAgICAgICAgICAgICAgIHN3YXBWYWx1ZSA8LSB6CiAgICAgICAgICAgICAgICBzd2FwSW5jdW1iZW50IDwtIHRlbXAKICAgICAgICAgICAgICB9CiAgICAgICAgICAgICAgIyBBIHN1Y2Nlc3NmdWwgc3dhcCBtZWFucyB3ZSBicmVhayBvdXQgb2YgdGhlIGZvciBsb29wcyBhbmQgY29udGludWUgbG9va2luZyBmb3Igc3dhcHMuCiAgICAgICAgICAgICAgdHJ5U3dhcHBpbmcgPC0gVFJVRQogICAgICAgICAgICAgIHJlc3RhcnQgPC0gVFJVRQogICAgICAgICAgICAgIGJyZWFrICMgZXhpdHMgdGhlIGlubmVyIGZvciBsb29wCiAgICAgICAgICAgIH0KICAgICAgICAgIH0KICAgICAgICAgIGlmIChyZXN0YXJ0KSBicmVhayAgIyBicmVha3Mgb3V0IG9mIHRoZSBvdXRlciBmb3IgbG9vcAogICAgICAgIH0KICAgICAgfQogICAgfQogIH0sCiAgdGltZW91dCA9IHRpbWVMaW1pdCwgY3B1ID0gSW5mLCBvblRpbWVvdXQgPSAic2lsZW50IikKY2F0KCJIZXVyaXN0aWMgaW5jdW1iZW50IHZhbHVlID0gIiwgc3dhcFZhbHVlLCAiXG4iKQpjYXQoIkhldXJpc3RpYyBzb2x1dGlvbiBmb3IgQSA9ICIsIHN3YXBJbmN1bWJlbnQsICJcbiIpCmBgYAoKIyBNSUxQIG1vZGVsCgpUaGUgTUlMUCBtb2RlbCB1c2VzIGEgMC0xIHZhcmlhYmxlICgkeCQpIGZvciBlYWNoIG5vZGUgKDEgaWYgdGhlIG5vZGUgYmVsb25ncyB0byAkQSQsIDAgaWYgbm90KSBhbmQgYSBjb250aW51b3VzIHZhcmlhYmxlICgkeSQpIHdpdGggZG9tYWluICRbMCwxXSQgZm9yIGVhY2ggZWRnZSAoMSBpZiB0aGUgZWRnZSBpcyBwYXJ0IG9mIHRoZSBjdXQsIDAgaWYgbm90KS4KCmBgYHtyfQojIEluaXRpYWxpemUgdGhlIG1vZGVsLgptaXAgPC0gTUlMUE1vZGVsKCkgfD4KIyBBZGQgdGhlIHZhcmlhYmxlcy4KICAgICAgICAgYWRkX3ZhcmlhYmxlKHgudmFyW2ldLCBpID0gMTpOLCB0eXBlID0gImJpbmFyeSIpIHw+CiAgICAgICAgIGFkZF92YXJpYWJsZSh5LnZhcltpXSwgaSA9IDE6bkVkZ2VzLCB0eXBlID0gImNvbnRpbnVvdXMiLCBsYiA9IDAsIHViID0gMSkgfD4KIyBTZXQgdGhlIG9iamVjdGl2ZSBmdW5jdGlvbi4KICAgICAgICAgc2V0X29iamVjdGl2ZShzdW1fZXhwcih5LnZhcltpXSwgaSA9IDE6bkVkZ2VzKSwgc2Vuc2UgPSAibWF4IikgfD4KIyBBZGQgdGhlIGJ1ZGdldCBjb25zdHJhaW50LgogICAgICAgICBhZGRfY29uc3RyYWludChzdW1fZXhwcih4LnZhcltpXSwgaSA9IDE6TikgPT0gYikKIyBGb3IgZWFjaCBlZGdlLCBhZGQgdHdvIGNvbnN0cmFpbnRzIGNvbm5lY3RpbmcgdGhlIHkgdmFyaWFibGUgZm9yIHRoYXQgZWRnZSB0byB0aGUgeCB2YXJpYWJsZXMgZm9yIGl0cyB0d28gZW5kcG9pbnRzLgpmb3IgKGkgaW4gMTpuRWRnZXMpIHsKICBsbyA8LSBlZGdlc1tpLCAibG8iXQogIGhpIDwtIGVkZ2VzW2ksICJoaSJdCiAgbWlwIDwtIG1pcCB8PgogICAgICAgICAgIGFkZF9jb25zdHJhaW50KHkudmFyW2ldIC0geC52YXJbbG9dIC0geC52YXJbaGldIDw9IDApIHw+CiAgICAgICAgICAgYWRkX2NvbnN0cmFpbnQoeS52YXJbaV0gKyB4LnZhcltsb10gKyB4LnZhcltoaV0gPD0gMikKfQp0aWMoIkNQTEVYIHJ1biB0aW1lIikKIyBTb2x2ZSB0aGUgbW9kZWwuCnNvbHV0aW9uIDwtIG1pcCB8PiBzb2x2ZV9tb2RlbCh3aXRoX1JPSShzb2x2ZXIgPSAiY3BsZXgiKSkKdG9jKCkKIyBHZXQgdGhlIG9iamVjdGl2ZSB2YWx1ZSAoY3V0IHNpemUpLgpjYXQoIlRoZSBvcHRpbWFsIGN1dCBzaXplID0gIiwgc29sdXRpb24kb2JqZWN0aXZlX3ZhbHVlLCAiXG4iKQojIFJlY292ZXIgdGhlIGNob2ljZSBvZiBBLgp0ZW1wIDwtIChzb2x1dGlvbiB8PiBnZXRfc29sdXRpb24oeC52YXJbal0pKVssICJ2YWx1ZSJdCkEgPC0gd2hpY2godGVtcCA+IDAuNSkKcm0odGVtcCwgbG8sIGhpKQpjYXQoIlRoZSBvcHRpbWFsIGNob2ljZSBmb3IgQSA9ICIsIEEsICJcbiIpCmBgYAoK