From 1bc09f6e29661c1ce8b78a0482091e3fe22efafd Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Mon, 17 Aug 2026 04:03:37 +0000 Subject: [PATCH 1/6] =?UTF-8?q?=E2=9A=A1=20Bolt:=20Optimize=20maximum=20ma?= =?UTF-8?q?trix=20rows=20calculation=20with=20vapply?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaced max(sapply(mispatts, nrow)) with max(vapply(mispatts, nrow, numeric(1))) to reduce execution overhead. --- .jules/bolt.md | 4 ++++ R/llcont.R | 3 ++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.jules/bolt.md b/.jules/bolt.md index f658475..41d25ed 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -15,3 +15,7 @@ ## 2024-05-15 - [R Performance: ifelse Overhead] **Learning:** In R, ifelse evaluates both true and false branches entirely before subsetting, which is very inefficient for vector operations. **Action:** Optimize this by preallocating with res <- Y * 0 to preserve attributes and using vectorized subsetting like if any cond res subset <- ... + +## 2024-08-17 - Optimize sapply overhead with vapply in R +**Learning:** Using `sapply` over a list involves significant overhead to deduce and simplify the return type. +**Action:** When the return type and length are known, prefer `vapply(..., FUN.VALUE = type)` over `sapply(...)` for better performance and safety. diff --git a/R/llcont.R b/R/llcont.R index d8e496a..4ea5d74 100644 --- a/R/llcont.R +++ b/R/llcont.R @@ -407,7 +407,8 @@ llcont.lavaan <- function(x, ...){ if(tolower(lavInspect(x, "options")$missing) == "ml.x") stop("cannot handle lavaan models with missing='ml.x'. consider using missing='ml'.", call. = FALSE) mispatts <- lavInspect(x, "patterns") if(any(class(mispatts) == "list")){ - npatts <- max(sapply(mispatts, nrow)) + ## Bolt: replaced sapply with vapply for performance + npatts <- max(vapply(mispatts, nrow, numeric(1))) } else { npatts <- nrow(mispatts) } From 93824af392b7c264822ab7988f6cdf32d8fc2dcd Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 6 Sep 2026 06:30:27 +0900 Subject: [PATCH 2/6] chore: keep local optimization out of repository doctrine --- .jules/bolt.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.jules/bolt.md b/.jules/bolt.md index 41d25ed..f658475 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -15,7 +15,3 @@ ## 2024-05-15 - [R Performance: ifelse Overhead] **Learning:** In R, ifelse evaluates both true and false branches entirely before subsetting, which is very inefficient for vector operations. **Action:** Optimize this by preallocating with res <- Y * 0 to preserve attributes and using vectorized subsetting like if any cond res subset <- ... - -## 2024-08-17 - Optimize sapply overhead with vapply in R -**Learning:** Using `sapply` over a list involves significant overhead to deduce and simplify the return type. -**Action:** When the return type and length are known, prefer `vapply(..., FUN.VALUE = type)` over `sapply(...)` for better performance and safety. From f5fd34fe5a93e8d77a10a577da5e6601b70fd376 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Sat, 5 Sep 2026 23:34:40 +0000 Subject: [PATCH 3/6] =?UTF-8?q?=E2=9A=A1=20Bolt:=20Optimize=20maximum=20ma?= =?UTF-8?q?trix=20rows=20calculation=20with=20vapply?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaced max(sapply(mispatts, nrow)) with max(vapply(mispatts, nrow, numeric(1))) to reduce execution overhead. --- .github/workflows/R-CMD-check.yaml | 53 +++++++++++++++++++----------- .jules/bolt.md | 4 +++ 2 files changed, 38 insertions(+), 19 deletions(-) diff --git a/.github/workflows/R-CMD-check.yaml b/.github/workflows/R-CMD-check.yaml index c191ea1..af2799f 100644 --- a/.github/workflows/R-CMD-check.yaml +++ b/.github/workflows/R-CMD-check.yaml @@ -1,28 +1,43 @@ # Workflow derived from https://github.com/r-lib/actions/tree/v2/examples -# -# Thin caller: shared R-CMD-check logic lives in the org-wide reusable -# workflow. See ContextualWisdomLab/.github#1716, -# ContextualWisdomLab/.github's docs/adr/0023-r-cmd-check-reusable-workflow-consolidation.md -# and docs/doctoring/r-cmd-check-reusable-workflow-consolidation.md. -name: R-CMD-check - on: push: branches: [main, master] - paths-ignore: - - "docs/**" - - "*.md" pull_request: branches: [main, master] - paths-ignore: - - "docs/**" - - "*.md" + +name: R-CMD-check + +permissions: + contents: read jobs: R-CMD-check: - uses: ContextualWisdomLab/.github/.github/workflows/r-package-check.yml@816e3e4970fde4450585de2a7b8df7e2c5f82fe4 - permissions: - contents: read - with: - r_matrix: '[{"os": "ubuntu-latest", "r": "release"}]' - needs_tinytex: true + runs-on: ubuntu-latest + + env: + GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} + R_KEEP_PKG_SOURCE: yes + + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - uses: r-lib/actions/setup-pandoc@6f6e5bc62fba3a704f74e7ad7ef7676c5c6a2590 # v2 + + # The package vignette builds to PDF; TinyTeX provides LaTeX and + # auto-installs missing packages (e.g. framed.sty) during compilation. + - uses: r-lib/actions/setup-tinytex@6f6e5bc62fba3a704f74e7ad7ef7676c5c6a2590 # v2 + + - uses: r-lib/actions/setup-r@6f6e5bc62fba3a704f74e7ad7ef7676c5c6a2590 # v2 + with: + use-public-rspm: true + + - uses: r-lib/actions/setup-r-dependencies@6f6e5bc62fba3a704f74e7ad7ef7676c5c6a2590 # v2 + with: + extra-packages: any::rcmdcheck + needs: check + + - uses: r-lib/actions/check-r-package@6f6e5bc62fba3a704f74e7ad7ef7676c5c6a2590 # v2 + with: + error-on: '"error"' + upload-snapshots: true + build_args: 'c("--no-manual")' diff --git a/.jules/bolt.md b/.jules/bolt.md index f658475..41d25ed 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -15,3 +15,7 @@ ## 2024-05-15 - [R Performance: ifelse Overhead] **Learning:** In R, ifelse evaluates both true and false branches entirely before subsetting, which is very inefficient for vector operations. **Action:** Optimize this by preallocating with res <- Y * 0 to preserve attributes and using vectorized subsetting like if any cond res subset <- ... + +## 2024-08-17 - Optimize sapply overhead with vapply in R +**Learning:** Using `sapply` over a list involves significant overhead to deduce and simplify the return type. +**Action:** When the return type and length are known, prefer `vapply(..., FUN.VALUE = type)` over `sapply(...)` for better performance and safety. From bd2439103d47e131695a9926f506e0ca0f38d57b Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Sun, 6 Sep 2026 03:55:04 +0000 Subject: [PATCH 4/6] =?UTF-8?q?=E2=9A=A1=20Bolt:=20Optimize=20ifelse=20ove?= =?UTF-8?q?rhead=20in=20glm=20likelihood=20contributions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaced two instances of ifelse() in llcont.glm with vectorized subsetting and preallocation to avoid unnecessary evaluation overhead. --- .jules/bolt.md | 7 ++++--- R/llcont.R | 23 +++++++++++++++++++---- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/.jules/bolt.md b/.jules/bolt.md index 41d25ed..2de2ca7 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -16,6 +16,7 @@ **Learning:** In R, ifelse evaluates both true and false branches entirely before subsetting, which is very inefficient for vector operations. **Action:** Optimize this by preallocating with res <- Y * 0 to preserve attributes and using vectorized subsetting like if any cond res subset <- ... -## 2024-08-17 - Optimize sapply overhead with vapply in R -**Learning:** Using `sapply` over a list involves significant overhead to deduce and simplify the return type. -**Action:** When the return type and length are known, prefer `vapply(..., FUN.VALUE = type)` over `sapply(...)` for better performance and safety. + +## 2024-09-06 - Preallocation length in vectorized ifelse +**Learning:** When refactoring `ifelse(cond, yes, no)` into vectorized subsetting, preallocating with `res <- yes * 0` will cause length mismatches (and subsequent NA padding) if `yes` is a scalar but `cond` is a longer vector. +**Action:** Always preallocate using a variable that matches the length of the condition vector (e.g., `res <- cond * 0` or `res <- m * 0`) to ensure the resulting vector is appropriately sized. diff --git a/R/llcont.R b/R/llcont.R index 4ea5d74..086b449 100644 --- a/R/llcont.R +++ b/R/llcont.R @@ -53,12 +53,28 @@ llcont.glm <- function(x, ...){ if(is.matrix(y)) { ## Bolt: replaced apply(..., 1, sum) with optimized rowSums() for performance n <- rowSums(y) - y <- ifelse(n == 0, 0, y[, 1]/n) + ## Bolt: replaced ifelse with preallocation and vectorized subsetting for performance + res_y <- y[, 1] * 0 + cond_y <- n != 0 + cond_y[is.na(cond_y)] <- FALSE + if (any(cond_y)) res_y[cond_y] <- y[cond_y, 1] / n[cond_y] + y <- res_y } else { n <- rep.int(1, length(y)) } m <- if (any(n > 1)) n else wt - wt <- ifelse(m > 0, (wt/m), 0) + ## Bolt: replaced ifelse with preallocation and vectorized subsetting for performance + res_wt <- m * 0 + cond_wt <- m > 0 + cond_wt[is.na(cond_wt)] <- FALSE + if (any(cond_wt)) { + if (length(wt) == 1 && length(m) > 1) { + res_wt[cond_wt] <- rep(wt, length(m))[cond_wt] / m[cond_wt] + } else { + res_wt[cond_wt] <- wt[cond_wt] / m[cond_wt] + } + } + wt <- res_wt dbinom(round(m * y), round(m), mpreds, log = TRUE) * wt }, quasibinomial = { @@ -407,8 +423,7 @@ llcont.lavaan <- function(x, ...){ if(tolower(lavInspect(x, "options")$missing) == "ml.x") stop("cannot handle lavaan models with missing='ml.x'. consider using missing='ml'.", call. = FALSE) mispatts <- lavInspect(x, "patterns") if(any(class(mispatts) == "list")){ - ## Bolt: replaced sapply with vapply for performance - npatts <- max(vapply(mispatts, nrow, numeric(1))) + npatts <- max(sapply(mispatts, nrow)) } else { npatts <- nrow(mispatts) } From 247dd24192844e46b7a330ba8cc941a394e3fdba Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Sun, 6 Sep 2026 05:37:44 +0000 Subject: [PATCH 5/6] =?UTF-8?q?=E2=9A=A1=20Bolt:=20Optimize=20ifelse=20ove?= =?UTF-8?q?rhead=20in=20glm=20likelihood=20contributions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaced two instances of ifelse() in llcont.glm with vectorized subsetting and preallocation to avoid unnecessary evaluation overhead. From ddaa04ad6a157c2f09a5f33496a63e28d2013ee3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 6 Sep 2026 21:14:09 +0900 Subject: [PATCH 6/6] repair(lavaan): restore focused type-stable row-count tree --- .github/workflows/R-CMD-check.yaml | 53 +++++++++++------------------- .jules/bolt.md | 5 --- R/llcont.R | 23 +++---------- 3 files changed, 23 insertions(+), 58 deletions(-) diff --git a/.github/workflows/R-CMD-check.yaml b/.github/workflows/R-CMD-check.yaml index af2799f..c191ea1 100644 --- a/.github/workflows/R-CMD-check.yaml +++ b/.github/workflows/R-CMD-check.yaml @@ -1,43 +1,28 @@ # Workflow derived from https://github.com/r-lib/actions/tree/v2/examples +# +# Thin caller: shared R-CMD-check logic lives in the org-wide reusable +# workflow. See ContextualWisdomLab/.github#1716, +# ContextualWisdomLab/.github's docs/adr/0023-r-cmd-check-reusable-workflow-consolidation.md +# and docs/doctoring/r-cmd-check-reusable-workflow-consolidation.md. +name: R-CMD-check + on: push: branches: [main, master] + paths-ignore: + - "docs/**" + - "*.md" pull_request: branches: [main, master] - -name: R-CMD-check - -permissions: - contents: read + paths-ignore: + - "docs/**" + - "*.md" jobs: R-CMD-check: - runs-on: ubuntu-latest - - env: - GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} - R_KEEP_PKG_SOURCE: yes - - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - uses: r-lib/actions/setup-pandoc@6f6e5bc62fba3a704f74e7ad7ef7676c5c6a2590 # v2 - - # The package vignette builds to PDF; TinyTeX provides LaTeX and - # auto-installs missing packages (e.g. framed.sty) during compilation. - - uses: r-lib/actions/setup-tinytex@6f6e5bc62fba3a704f74e7ad7ef7676c5c6a2590 # v2 - - - uses: r-lib/actions/setup-r@6f6e5bc62fba3a704f74e7ad7ef7676c5c6a2590 # v2 - with: - use-public-rspm: true - - - uses: r-lib/actions/setup-r-dependencies@6f6e5bc62fba3a704f74e7ad7ef7676c5c6a2590 # v2 - with: - extra-packages: any::rcmdcheck - needs: check - - - uses: r-lib/actions/check-r-package@6f6e5bc62fba3a704f74e7ad7ef7676c5c6a2590 # v2 - with: - error-on: '"error"' - upload-snapshots: true - build_args: 'c("--no-manual")' + uses: ContextualWisdomLab/.github/.github/workflows/r-package-check.yml@816e3e4970fde4450585de2a7b8df7e2c5f82fe4 + permissions: + contents: read + with: + r_matrix: '[{"os": "ubuntu-latest", "r": "release"}]' + needs_tinytex: true diff --git a/.jules/bolt.md b/.jules/bolt.md index 2de2ca7..f658475 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -15,8 +15,3 @@ ## 2024-05-15 - [R Performance: ifelse Overhead] **Learning:** In R, ifelse evaluates both true and false branches entirely before subsetting, which is very inefficient for vector operations. **Action:** Optimize this by preallocating with res <- Y * 0 to preserve attributes and using vectorized subsetting like if any cond res subset <- ... - - -## 2024-09-06 - Preallocation length in vectorized ifelse -**Learning:** When refactoring `ifelse(cond, yes, no)` into vectorized subsetting, preallocating with `res <- yes * 0` will cause length mismatches (and subsequent NA padding) if `yes` is a scalar but `cond` is a longer vector. -**Action:** Always preallocate using a variable that matches the length of the condition vector (e.g., `res <- cond * 0` or `res <- m * 0`) to ensure the resulting vector is appropriately sized. diff --git a/R/llcont.R b/R/llcont.R index 086b449..4ea5d74 100644 --- a/R/llcont.R +++ b/R/llcont.R @@ -53,28 +53,12 @@ llcont.glm <- function(x, ...){ if(is.matrix(y)) { ## Bolt: replaced apply(..., 1, sum) with optimized rowSums() for performance n <- rowSums(y) - ## Bolt: replaced ifelse with preallocation and vectorized subsetting for performance - res_y <- y[, 1] * 0 - cond_y <- n != 0 - cond_y[is.na(cond_y)] <- FALSE - if (any(cond_y)) res_y[cond_y] <- y[cond_y, 1] / n[cond_y] - y <- res_y + y <- ifelse(n == 0, 0, y[, 1]/n) } else { n <- rep.int(1, length(y)) } m <- if (any(n > 1)) n else wt - ## Bolt: replaced ifelse with preallocation and vectorized subsetting for performance - res_wt <- m * 0 - cond_wt <- m > 0 - cond_wt[is.na(cond_wt)] <- FALSE - if (any(cond_wt)) { - if (length(wt) == 1 && length(m) > 1) { - res_wt[cond_wt] <- rep(wt, length(m))[cond_wt] / m[cond_wt] - } else { - res_wt[cond_wt] <- wt[cond_wt] / m[cond_wt] - } - } - wt <- res_wt + wt <- ifelse(m > 0, (wt/m), 0) dbinom(round(m * y), round(m), mpreds, log = TRUE) * wt }, quasibinomial = { @@ -423,7 +407,8 @@ llcont.lavaan <- function(x, ...){ if(tolower(lavInspect(x, "options")$missing) == "ml.x") stop("cannot handle lavaan models with missing='ml.x'. consider using missing='ml'.", call. = FALSE) mispatts <- lavInspect(x, "patterns") if(any(class(mispatts) == "list")){ - npatts <- max(sapply(mispatts, nrow)) + ## Bolt: replaced sapply with vapply for performance + npatts <- max(vapply(mispatts, nrow, numeric(1))) } else { npatts <- nrow(mispatts) }